internal/api/user_preferences.go. Persisted in user_preferences (migration v58) as (user_id, pref_key, pref_value, updated_at) with a UNIQUE(user_id, pref_key) constraint.
Endpoints
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /api/v1/me/preferences | List all preferences for the user |
| PUT | /api/v1/me/preferences/{key} | Upsert one preference key |
| DELETE | /api/v1/me/preferences/{key} | Delete one preference key |
Auth and limits
- All endpoints require an authenticated session.
- The
user_idis taken from the session — never from request bodies or query parameters. - Key constraint:
[a-zA-Z0-9._-]{1,64}. Anything else returns400. 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
413 Request Entity Too Large. - There is no admin endpoint to read another user’s preferences.
List preferences
key → JSON value. Missing keys are simply absent from the map; the frontend falls back to its compiled-in defaults.
Response: 200 OK
json.RawMessage so the FE doesn’t have to JSON-decode each value separately.
Errors:
| Status | Condition |
|---|---|
| 401 | Not authenticated. |
| 500 | DB error or invalid JSON in a stored row (corruption — should not happen). |
Set one preference
{value: …} wrapper. So setting ui.theme to "dark" is:
crews.bottom_panel_height:
204 No Content on success.
Errors:
| Status | Condition |
|---|---|
| 400 | Invalid key (regex), or empty body. |
| 401 | Not authenticated. |
| 413 | Body exceeds the 16 KB cap. |
| 500 | DB error. |
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
(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:
| Status | Condition |
|---|---|
| 400 | Invalid key. |
| 401 | Not authenticated. |
| 500 | DB 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:| Key | Type | Purpose |
|---|---|---|
ui.theme | string | "light", "dark", or "system". |
ui.density | string | "compact", "comfortable", or "spacious". |
crews.bottom_panel_height | number | Pixel 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_enter | bool | If false, Enter inserts newline and Cmd-Enter sends. |
chat.last_session_per_agent | object | Map agentId → chatId so the chat surface re-opens the last session per agent. |
<surface>.<setting>) so a future grep stays scannable.
Related
- Chat & Sessions guide — uses
chat.*keys. - Migrations — v58 —
user_preferencesschema.