Docyrus App Settings
Two database-backed settings documents you can attach to a Docyrus app. The
shape of what you store is entirely yours — each document holds a free-form
JSON object (data) that you design, plus a small numeric status flag. Docyrus
just persists and returns it per app (and, for the user variant, per user).
| Surface | Scope | Cardinality | Who sees it | Use for |
|---|---|---|---|---|
App config (AppConfig) |
The whole app | one document per app | every user in the tenant | app-wide settings an admin/builder configures: feature flags, defaults, shared filters, integration settings |
User config (UserAppConfig) |
The app and the signed-in user | one document per user per app | only that user | per-user preferences: theme, layout, sidebar state, onboarding/dismissed hints, saved personal defaults |
Both are upsert-only single documents — there is no list, no query, and no extra rows to manage. You read the one document, change it, write it back.
Choosing the scope
- Shared across everyone, one source of truth for the app → app config. Reading is broadly allowed; writing needs a tenant-admin-level token (see scopes below), because one user's write changes it for all.
- Personal to the current user, different per person → user config. Any signed-in user can read and write their own; there is no way to read or write another user's document through the API.
If a setting is really a tenant-wide regional/formatting preference (language,
date/number/currency formats, working hours), that is not this — use
docyrus-account-settings (tenant-preferences) instead.
Document shape
Both documents return the same envelope of fields (user config adds user_id):
interface AppConfig {
id: string; // "" when no document exists yet (see gotcha)
data: Record<string, unknown>; // YOUR settings object — design it freely
status: number; // small integer flag you define; defaults to 1 on first write
tenant_app_id: string; // the app this belongs to
created_by: string | null;
created_on: string; // ISO timestamp
last_modified_by: string | null;
last_modified_on: string | null;
}
interface UserAppConfig extends AppConfig {
user_id: string; // always the signed-in user
}
Rules that hold for both:
datamust be a JSON object ({...}) — not an array, string, or number.datais replaced wholesale on write, not deep-merged. To change one key, read the currentdata, spread it, and write the merged object back.statusis a free small integer (e.g.1= active,0= disabled). It defaults to1when the document is first created; on later writes it only changes if you send it.
Primary path: @docyrus/app-utils clients
In a Docyrus React/TypeScript app, use the config clients from
@docyrus/app-utils. Each is built from a RestApiClient (from
@docyrus/api-client) plus the app's id, and exposes get / upsert /
remove. They unwrap the API envelope for you and return the typed document.
import { createAppConfigClient, createUserAppConfigClient } from '@docyrus/app-utils'
const appConfig = createAppConfigClient(client, appId) // shared, app-wide
const userConfig = createUserAppConfigClient(client, appId) // private, per-user
Client surface (identical for both):
appConfig.get(): Promise<AppConfig> // never throws on "empty" — returns id: ""
appConfig.upsert(body: { data?; status? }): Promise<AppConfig> // create or update
appConfig.remove(): Promise<void> // deletes; throws 404 if nothing exists
Design your own settings and use them
// 1. Design the settings you want to persist.
interface MyAppSettings {
defaultView: 'board' | 'table'
notificationsEnabled: boolean
pinnedFilters: string[]
}
// 2. Read (with a typed cast over the free-form `data`).
const doc = await userConfig.get()
const settings = doc.data as Partial<MyAppSettings>
const view = settings.defaultView ?? 'table' // apply your own defaults
// 3. Change ONE key — read-merge-write, because `data` is replaced wholesale.
await userConfig.upsert({ data: { ...doc.data, defaultView: 'board' } })
// 4. Reset to defaults.
await userConfig.remove()
App config is the same, just a different client — write app-wide defaults an admin sets once and every user reads:
await appConfig.upsert({ data: { onboardingFlow: 'v2', maxUploadMb: 25 } })
The "empty document" gotcha
get() always resolves — when no document exists yet it returns a blank one
with id: "", data: {}, status: 0. It does not throw or return
null. Detect "not configured yet" by checking the id:
const doc = await appConfig.get()
if (!doc.id) {
// no config saved yet — show setup UI / seed defaults
}
Optional: shared caching via the inventory client
For an app that reads these documents in many places, pass a shared
InventoryClient so reads are cached and writes patch the cache in place
(no refetch):
const inventory = createInventoryClient({ client })
const appConfig = createAppConfigClient(client, appId, { inventory })
const userConfig = createUserAppConfigClient(client, appId, { inventory })
upsert/remove keep the inventory consistent automatically. Wire these
clients into your data layer (e.g. a TanStack Query queryFn) the same way as
the other @docyrus/app-utils runtime helpers — see docyrus-app-dev-react
for the full app-runtime wiring pattern and how to obtain client/appId.
Alternative path: raw REST / CLI
When you are not in a @docyrus/app-utils app (a different language, a script,
a quick check), call the endpoints directly. There is no dedicated docyrus
subcommand for these — use docyrus curl from the terminal:
# resolve your app's id if you only know the slug
docyrus apps list --format json
# read / write / delete the app-wide config
docyrus curl /v1/tenant/apps/<appId>/config --format json
docyrus curl -X PUT /v1/tenant/apps/<appId>/config -d '{"data":{"maxUploadMb":25}}'
docyrus curl -X DELETE /v1/tenant/apps/<appId>/config
# the current user's private config
docyrus curl /v1/tenant/apps/<appId>/user-config --format json
docyrus curl -X PUT /v1/tenant/apps/<appId>/user-config -d '{"data":{"theme":"dark"}}'
The response is an envelope whose data is the document — and the document
carries its own data key with your settings, so your values live at
response.data.data. Full HTTP contract (all six endpoints, request/response
bodies, status codes, OAuth2 scopes, error shapes) is in
references/rest-api.md.
Key rules
- Two scopes, one document each.
config= shared app-wide;user-config= private to the caller. Pick by "should everyone see the same value?". datais your design — any JSON object. Version it yourself (e.g. aschemaVersionkey) if it will evolve.- Writes replace
datawholesale. Change one field with read-merge-write ({ ...doc.data, key: value }); a bareupsert({ data: {...} })drops any keys you omit. get()never fails on empty — checkid === ""(!doc.id) to detect an unconfigured app;remove()does throw404when there is nothing to delete.- User config is self-only. It always targets the signed-in user; you cannot address another user's document.
- App-config writes need an admin-scoped token; user-config writes only need the user's own token. See scopes in references/rest-api.md.
- Settings values are at
response.data.dataover raw REST (envelopedata→ document → its owndata). The@docyrus/app-utilsclients hide the outer envelope, so there you readdoc.data.
Related skills
- docyrus-app-dev-react — building the app UI; how to obtain
clientandappIdand wire the app-utils runtime (dates, numbers, data views, these config clients). - docyrus-api-dev — constructing a
RestApiClientand OAuth2 auth for non-app callers. - docyrus-account-settings — tenant-wide regional & formatting preferences and the user's own profile (a different surface).
- docyrus-cli-app — full
docyrusCLI reference (curl,apps, auth, environments).