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.
Notification preferences
/me/preferences is a schemaless,
never-workspace-scoped UI blob; /me/notification-prefs is a workspace-scoped
category × channel matrix — which of your notification channels (email,
webhook, Slack, Discord, Telegram, …) get pinged for which category of event
(approvals, escalations, failed routines, chat replies, …). See the
Outbound notifications guide for the full category
list, channel setup, and the two-layer (admin allowlist + user preference)
model. CLI parity: crewship notify prefs.
Implementation: internal/api/notification_prefs.go, backed by
internal/notifyroute.PrefStore over the user_notification_prefs table
(migration v154).
Auth: authenticated session with workspace context (GET/PUT both
require workspace_id + the caller’s own user id from context — there is no
path or body parameter naming a different user).
Get your matrix
200 OK
cells is [] (never null) when nothing has been set. Each cell’s state
is off or immediate (digest is schema-reserved for a v2 digest window;
this MVP never reads or writes it).
Errors:
Set cells in your matrix
{ "cells": [{ "category", "channel_id", "state" }] } — one
or more cells to upsert.
PUT upserts the cells you send — it is not a full-matrix replace, so a
single-cell edit never needs to resend every other cell (unsent cells keep
whatever state they already had).
category is one of the values in Conventional categories
below, or * — the per-channel “mute everything” sentinel cell. channel_id
must name a channel the workspace already has (crewship notifychannel add).
state is off or immediate.
Response: 200 OK — { "ok": true }.
Errors:
Conventional categories
The fixed vocabulary — shared by this matrix, the admin per-channel allowlist, and the delivery log.internal/notify.AllCategories, grouped
the same way the settings UI and crewship notify prefs render them:
* is a sentinel, not a real category — legal as a cell’s category (mutes
every category on that channel) but never appears in the allowlist or a
delivery log row. agents.message (an agent’s own notify_send tool call)
is deliberately not in this matrix at all — it is routed by the
agent-channel pairing that
authorized it, not by a user’s mute state.
Related
- Chat & Sessions guide — uses
chat.*keys. - Migrations — v58 —
user_preferencesschema.