Accepted 2026-10-31 by the owner: “all accepted merege and proceed” (STA-1371); owner direction
and answers of 2026-10-31. Amends 0108 S1 and S9,
0213 decision 2,
0307 rules 22 and
24, and 0302 (the 2026-10-29 note
on text size). Adds a note to 0080. Under
0003, moving a member between categories is an ADR; this is
that record.
Context
@stackra/settings lives in packages/state/settings and depends on ui (only for the native SettingsForm),
icons (only for the SettingsNavigation icon type), and database and model (only for ./node). So theming, a
light web client or a native app cannot use it, and no UI preference is a setting today: the colour scheme, language,
digits, brand, transition and haptics live in MMKV on mobile, in localStorage in the playground, and in the prefs
storage area in theming. AccountSettings (account.locale) is declared inside the settings package, which owns no
domain. The owner: “settings package should be a foundation package for frontend and node … each service registers
its settings … account … should be under identity or user”, and “for any UI configurations make it dynamic config we
can set from the dashboard”, “and in the theme builder also”. The design is
notes/design-system-research-2026-10-31/ui-settings-design.md.
Decision
The split.- Settings core is foundation.
@stackra/settingsmoves topackages/foundation/settingsand keeps its name and its.,./contracts,./testingand./nodeentry points. The core depends only on foundation members (contracts,container,errors,validation,i18n,market-packs) pluszodandyaml. It never imports@stackra/uior@stackra/icons:SettingsNavigation.iconis an icon-name string, and gen checks the name. ./nodekeeps the Postgres drivers:StoredSetting, the value and revision stores, the migrations, andsettingsDiffandsettingsApply.@stackra/databaseand@stackra/modelare optional peer dependencies used only by./node, so a web or native install pulls neither../reactholds hooks only (useSettings,useSettingsStore), with React as an optional peer.- The forms move to
@stackra/settings-ui, a new experience member atpackages/experience/settings-uiwith./react(web) and./native: the genericSettingsForm, the generic settings route, the Preferences hub, the lock, provenance and entitlement badges, and the option-source registry. It depends onsettings,ui,i18n,iconsandnavigation.
-
The settings package declares no group. Each group is declared by the module that owns what its values mean, in
<module>.settings.ts(0307 rule 22, 0294): -
AccountSettingsmoves to@stackra/identityand keeps the group keyaccountand the pathaccount.locale, so stored rows (stored_settings.group_key = 'account') are not rewritten. It gains the Tenant scope and the additive keysnumerals(defaultlatn, 0192),calendar,hourCycle,dateStyle,firstDayOfWeek,timeZone,regionandcurrencyDisplay. -
One owner per group key. Gen fails with
settings_group_conflictwhen two members declare the same group key. -
Plain types stay where they are.
tokensanduiexpose plain preference types (ColorScheme,MotionLevel,MaterialMode, density) and take values as props.uinever imports settings; theming bridges the two.
SettingTypegainsChoice(a closedoptionslist, emitted by gen from an enum field, with option labels at<module>::settings.<path>.options.<value>) andList(an array of strings, optionally constrained to an option list). Gen refuses a field type it cannot map instead of dropping it. An option list may be dynamic through a named option source (theming.presets,theming.themes); the server still validates the stored value.@Settingtakes a closedcontrolhint (hue,swatch,preview).
- Device level.
SettingScope.Deviceis the innermost level, after User. A device value is stored only on the client and the server ignores it. The cascade is: default, platform, product, ancestor tenants, tenant, nodes, user, device. Locks and floors of every outer level apply to it. - Per device, or per account (owner Q3).
appearance.colorScheme,appearance.material,appearance.transition,appearance.densityandmotion.hapticsare written at Device by default. Every other person key is written at User. A “Use on all my devices” action copies the device values of a group to User. A page writes each field at its innermost allowed level (SettingsPagegains a per-field write level). - Client resolution. A client driver in the settings core resolves over the bootstrap snapshot (the merged outer
layers with their locks, then the user’s layer), then the device layer, with the same
mergeSettingLayersand schema parse as the server, so a locked key stays locked offline. It takes a structural{ get, set }handle, which@stackra/storage’sprefsarea satisfies, so the core never imports storage. The last snapshot per host and user is cached and read synchronously before the first frame; before sign-in the client uses the defaults, then the host’s public bootstrap (tenant values and locks, no user), then the device layer. Sign-out drops the user layer and keeps the device layer. - Live. A User write calls the owning module’s generated
settings.updateand applies optimistically. Every write and everysettings.changed.v1(0108 S7) received over realtime notifies subscribers. Theming’sThemeBridgefeedsDesignSystemProviderandI18nProviderfrom the resolved values, re-derives the theme and plays the transition. Nothing reloads. - Floors from holders (amends 0213). A floor may be declared by a holder (platform or tenant) as well as by a
market pack (0370): “may force on, never off” for the
accessibility.*switches andmotion.level(never higher than the floor). A tenant may raise a platform floor, never lower it. OS flags (reduce motion, increase contrast, reduce transparency) are combined on the client; settings store only the person’s choice.
- A group declared by a package is stored by the service that mounts that package’s module. In Figentra OS the
identity service mounts
account,appearance,motion,accessibility,layout,helpandshortcuts, so one bootstrap returns every person-preference group with its locks.
- Anything that configures UI is a setting at Platform or Tenant (and lower where it makes sense), lockable and
editable from the dashboard, and applies live with no deploy. This moves out of deploy-time
@stackra/config: theThemingConfigvalues (theme, default and offered schemes, logo and brand, the tenant colour switch,maxFontScale), the offered locales and default numerals, the allowed presets and themes, the material and motion defaults, and the transition. A deploy-timeThemingConfigorI18nConfigvalue becomes the packaged default of the matching setting during the transition and is then removed. - Deploy-time config keeps only infrastructure: what needs a rebuild or is the same for every scope of a deployment and is not a UI choice: bundled fonts and assets, icons, splash and web manifest files, the identity flow and device client, endpoints and credentials (credentials are never settings, 0307 rule 23).
- Defaults (owner Q4).
appearance.transitiondefaults tocircle-origin(the circle from the tap) and falls back tofadeunder Reduce Motion.appearance.materialdefaults by theme family: front themes areglass; studio themes aresolidwithglasson overlays.
appearance.textScale(number, 0.85 to 1.6, default 1) is an in-app text-size setting that multiplies the device’s own scale (Dynamic Type or the browser zoom) and never replaces it. The effective cap is theappearance.maxFontScalesetting (Platform, Product, Tenant; lockable; replacesThemingConfig.maxFontScale). The 0302 note’s “no in-app text-size setting” is withdrawn; the rest of that note stands.
- A theme built in Theme Studio is data: a
ThemeDocumentthat the tokens v2 schema validates. A custom theme is a named record owned by a tenant, or by the platform for a shared theme, with append-only revisions and a publish step. Settings revisions version one key’s value, not a named document, so they do not fit. Theming gains a small theme-records store (ThemeRecordStoreport, memory driver, a Postgres driver in./node):{ id, name, owner, draft, publishedRevision, revisions }. Publishing honours the same four-eyes approval as theappearance.themekey at that holder (0213 decision 5) and emitssettings.changed.v1for theappearancegroup. appearance.themeis a choice over the built-in presets and the saved theme ids visible to the holder (option sourcetheming.themes).appearance.allowedThemes(a List, Platform, Product, Tenant, lockable) restricts what users may pick; a stored choice that is no longer allowed reads as the holder’s default, as the entitlement fallback of 0420 does. A published theme applies live through the theme bridge’s live-apply function.- Admin-only keys (Platform, Product, Tenant):
appearance.brand,appearance.neutral.*,appearance.allowedThemes,appearance.schemes,appearance.maxFontScale,account.locales(the offered locales). Per-tenant brand stays Pro (0077): the keys are free code, and the Tenant level exists only where tenancy is installed.
- Gen collects groups from the member’s own modules and from every consumed manifest, emits
SettingsPages(an admin page per group at Tenant, or Platform in the console, and a person page at User and Device), registers one generic route per page (/settings/<group>, and/preferences/<group>for person pages) and a Preferences hub, all rendered by@stackra/settings-ui’sSettingsForm. No settings page is written by hand. Quick switchers may stay hand-built but read and write only throughuseSettings.
Consequences
- Any web, native or node member can declare and read settings; ui stays settings-free.
- A new module’s group appears in the dashboard and the Preferences hub with no frontend change.
- Theming, identity, panel and shortcuts gain a dependency on
@stackra/settings. - Stored
accountrows keep working. Mobile MMKV and playgroundlocalStoragekeys migrate once at launch (migrateDisplayPrefs), and the native transition becomes the string id everywhere. ThemingConfigandI18nConfigshrink to infrastructure over a transition; until then their UI values are only the packaged defaults.- Chores: the theme-records store and its Postgres migration, the realtime subscription, and the console pages for floors and allowed themes.
Enforcement
settings_group_conflictin gen (tools/gen), failing when two members declare one group key.- Gen refuses an unmappable settings field type (
settings_unsupported_type). - Boundaries: the core entry points of
@stackra/settingsmay not import@stackra/ui,@stackra/icons,@stackra/databaseor@stackra/model(pnpm boundaries). - planned — a gen check that no
ThemingConfigorI18nConfigfield configures UI once the transition ends. - planned — the client and server resolve-identically test (peer session).
Supersedes and sources
Supersedes: none. Amends: 0108 S1 and S9, 0213 decision 2, 0302 (the 2026-10-29 note on text size), 0307 rules 22 and 24. Notes: 0080. Sources:notes/design-system-research-2026-10-31/ui-settings-design.md; owner answers Q1 to Q4 and
the dynamic-config and theme-builder directions, 2026-10-31.