Skip to content

User management

Users are created and managed from the command line. This is the primary path when frontend registration is disabled (see REGISTRATION_ENABLED).

Create a user

make create-user -- --username john --email john@example.com --name "John Doe"
# Using short flags
make create-user -- -u john -e john@example.com -n "John Doe"

If a password is not provided via flag, you'll be prompted to enter it securely.

Create an admin user — grants the global admin role (run make migrations first so the role is seeded):

make create-user -- --username admin --email admin@example.com --name "Admin User" --admin

Provide the password directly:

make create-user -- -u john -e john@example.com -n "John Doe" --password "SecurePass123"

Options:

  • --username, -u: Username (required)
  • --email, -e: Email address (required)
  • --name, -n: Full name (optional, defaults to the username)
  • --password, -p: Password (optional, will prompt if not provided)
  • --email-verified: mark the user's email as already verified (optional, default: false)
  • --admin (alias --superuser): also grant the user the global admin role (optional, default: false). The admin role must already be seeded (via make migrations), or the command exits without creating the user. See the permissions guide for what the admin role grants.

Reset a password

The user is given as a positional username or email:

make reset-password -- john
# Provide the password directly
make reset-password -- john --new-password "NewSecurePass123"
make reset-password -- john -p "NewSecurePass123"

Options:

  • identifier: Username or email of the user (required, positional)
  • --new-password, -p: New password (optional, will prompt if not provided)

Preferred language

Each user has a preferred language recorded on their profile: a BCP 47 tag, readable and settable through the API. It selects the language of Sparkth's own interface text — labels, buttons, and fixed backend messages — from the languages the platform ships translations for.

The language of AI-generated text

Chat replies, generated course content, and conversation titles are not governed by this setting. The assistant writes in the language of the user's most recent message and switches when the user switches, so no configuration is needed and any language the model handles is available.

The translations guide explains how the two languages are resolved and why they are independent.

  • Chat replies follow the language the user is writing in.
  • Generated course content — titles, descriptions, lesson text, assessment questions, answer options and feedback — follows the same language.
  • Conversation titles follow the language of the message the conversation opened with.

A user writing in one language may ask for the course itself in another; the assistant honours that request for the course content and keeps replying in the language the user is writing in.

A source document keeps its own language. Uploading an English PDF while writing in Spanish produces a Spanish course generated from English source: the source is read as written, and only the output follows the conversation.

Quality varies by language. Sparkth places no restriction on which languages it will generate in, which means output in a language nobody on the team has reviewed is possible; large language models are measurably stronger in widely spoken languages than in low-resource ones.

Internal prompts are deliberately excluded — the scope classifier, the retrieval intent router and the document search agent all reason in English, the language current models are strongest in, while handling input in any language. Nothing they produce is shown to a user.

The Slack assistant is a separate case: it answers questions from Slack members, who are not signed-in Sparkth users, and its prompts carry no language directive at all.

The language of fixed backend text

API error messages and other fixed backend strings follow the interface language rather than the conversation, because no model is involved in producing them.

The out-of-scope refusal can reach a user through either path, and which one handles a given request decides which language rule applies. When the assistant model itself judges a request out of scope, the refusal is chat-generated text like any other reply, so it follows the language of the conversation. A faster check can also end the request before the assistant model is ever invoked; a classification step may run there, but it only decides whether the request is in scope — the backend writes the refusal sentence itself rather than a model, so that refusal follows the interface language instead. Either way the user sees the same sentence — only the language it arrives in depends on which path caught the request.

Language and the MCP server

The MCP server authenticates no user, so get_course_generation_prompt_tool has no conversation to read a language from. It accepts an optional language tag instead, which the calling agent supplies. Any BCP 47 tag is accepted, not only the languages the interface ships in; an omitted tag, or a value that is not a valid language tag, uses DEFAULT_LANGUAGE. A bad value never fails a generation run.

The tag is the course's language and says nothing about the language the agent's own user speaks — an English speaker may commission a Spanish course, and the agent should keep asking its clarifying questions in the language of its own conversation.

Course metadata on a publishing target

A course published to Open edX is tagged with the language the assistant generated it in: the course-creation tool asks the model for a BCP 47 language tag and applies it to the course's Open edX language setting once the run exists. Canvas courses are not tagged — its course locale is read-only over the REST API and can only be set through the Canvas UI, so the content is in the right language while the metadata is absent.

Supported languages

Tag Language
en English
es Español (Spanish)
fr Français (French)

The list is deliberately short. A language is added only once a full interface translation for it exists and has been reviewed. Inclusion is not a claim that the model is equally strong in every listed language: measured accuracy drops noticeably in less-represented ones. Matching against the list is an exact, case-sensitive comparison: en-US and EN are both rejected as unsupported, not normalised to en.

Fetch the current list — and the platform default — from the API. This endpoint needs no token, so the sign-in and password-reset pages can render in the right language before anyone has one:

curl http://localhost:7727/api/v1/languages
{
  "languages": [
    {"code": "en", "name": "English", "native_name": "English"},
    {"code": "es", "name": "Spanish", "native_name": "Español"},
    {"code": "fr", "name": "French", "native_name": "Français"}
  ],
  "default": "en"
}

Reading and setting a user's language

GET /api/v1/user/me returns language as stored: null means the user has never chosen one, which is different from having chosen English. The platform-wide fallback value is DEFAULT_LANGUAGE, configurable and also readable as default in the GET /api/v1/languages response (see the configuration reference).

Set it with:

curl -X PATCH http://localhost:7727/api/v1/user/me \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"language": "es"}'

An unsupported tag is rejected with a 422.

Clear a previously chosen language — so the platform default applies again — by sending an explicit null rather than omitting the field:

curl -X PATCH http://localhost:7727/api/v1/user/me \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"language": null}'

The new value is stored immediately and returned by subsequent reads.

If a language is later removed from the supported list, a stored tag that is no longer in the list stops being a valid choice: the value stays in the column, but it no longer passes the allowlist check, so PATCH rejects it with a 422 if the user tries to set it again.