Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.crewship.ai/llms.txt

Use this file to discover all available pages before exploring further.

User Preferences

User preferences are persisted in user_preferences (migration v58) as a string-keyed map of raw JSON values, scoped to the authenticated user. They follow the user across browsers, devices, and workspaces — preferences are never workspace-scoped. The handler is deliberately schemaless on the value side. The frontend writes whatever JSON shape it wants under a key; the server stores the bytes and hands them back unchanged on read. This keeps UI iteration fast — adding a new preference doesn’t need a migration or a backend release. Implementation: internal/api/user_preferences.go. Backed by user_preferences (user_id, pref_key, pref_value, updated_at) with a UNIQUE(user_id, pref_key) constraint.

Auth and limits

  • All endpoints require an authenticated session.
  • The user_id is taken from the session — never from request bodies or query parameters.
  • Key constraint: [a-zA-Z0-9._-]{1,64}. Anything else returns 400. Keys land in URL paths, so this is a defensive parse before they reach the SQL layer.
  • Value cap: 16 KB per key. Larger payloads return 400.
  • There is no admin endpoint to read another user’s preferences.

List preferences

GET /api/v1/me/preferences
Returns every preference row for the authenticated user as a flat map of key → JSON value. Missing keys are simply absent from the map; the frontend falls back to its compiled-in defaults. Response: 200 OK
{
  "ui.density": "comfortable",
  "ui.theme": "system",
  "crews.bottom_panel_height": 220,
  "chat.composer.send_on_enter": true,
  "chat.last_session_per_agent": {
    "agt_viktor": "cht_a1b2",
    "agt_eva": "cht_c3d4"
  }
}
Values are parsed JSON — a number stays a number, an object stays an object. The handler round-trips through json.RawMessage so the FE doesn’t have to JSON-decode each value separately. Errors:
StatusCondition
401Not authenticated.
500DB error or invalid JSON in a stored row (corruption — should not happen).

Set one preference

PUT /api/v1/me/preferences/{key}
Content-Type: application/json
Upserts one key. The request body is the raw JSON value to store — not a {value: …} wrapper. So setting ui.theme to "dark" is:
PUT /api/v1/me/preferences/ui.theme
Content-Type: application/json

"dark"
And setting crews.bottom_panel_height:
PUT /api/v1/me/preferences/crews.bottom_panel_height
Content-Type: application/json

220
Response: 204 No Content on success. Errors:
StatusCondition
400Invalid key (regex), body is not valid JSON, or body exceeds 16 KB.
401Not authenticated.
500DB error.
The PUT is atomic via SQLite UPSERT (INSERT … ON CONFLICT(user_id, pref_key) DO UPDATE), so concurrent writes from two devices don’t lose data — last write wins.

Delete one preference

DELETE /api/v1/me/preferences/{key}
Removes the row for (user_id, key). Idempotent — deleting a non-existent key returns 204, not 404. The frontend’s “reset to default” action just deletes the key. Response: 204 No Content. Errors:
StatusCondition
400Invalid key.
401Not authenticated.
500DB error.

Conventional keys

The server does not enforce these names, but the frontend uses them consistently. Listing them here so integrators know what to read/write:
KeyTypePurpose
ui.themestring"light", "dark", or "system".
ui.densitystring"compact", "comfortable", or "spacious".
crews.bottom_panel_heightnumberPixel height of the bottom panel in /crews. First consumer of this table — the schema is intentionally generic so further preferences land without another migration.
chat.composer.send_on_enterboolIf false, Enter inserts newline and Cmd-Enter sends.
chat.last_session_per_agentobjectMap agentId → chatId so the chat surface re-opens the last session per agent.
Adding a new key is purely a frontend change — no API or migration work needed. Use the dotted-namespace convention (<surface>.<setting>) so a future grep stays scannable.