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
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:
Set one preference
{value: …} wrapper. So setting ui.theme to "dark" is:
crews.bottom_panel_height:
204 No Content on success.
Errors:
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
{key} path parameter identifies the preference to delete for the authenticated user.
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:
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:
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.
Related
- Chat & Sessions guide — uses
chat.*keys. - Migrations — v58 —
user_preferencesschema.