Skip to content

docs(settings): define the cross-platform settings contract - #477

Closed
Quick104 wants to merge 5 commits into
mainfrom
docs/settings-contract-design
Closed

docs(settings): define the cross-platform settings contract#477
Quick104 wants to merge 5 commits into
mainfrom
docs/settings-contract-design

Conversation

@Quick104

Copy link
Copy Markdown
Contributor

Design document only. No code, no runtime behavior change.

What this adds

docs/superpowers/specs/2026-07-10-cross-platform-user-settings-contract-design.md — the decision-complete contract for how user settings work across the server, bundled web client, Apple clients, and Android clients. It turns the audit in #376 into something implementable.

The decisions it makes

Ownership. Every production user-facing setting needs a server-owned manifest entry, even when the value is only ever stored on one client. The single exception is private local.<client>.* diagnostics and experimental knobs, bounded by five conditions.

Types and scopes. Native JSON values instead of strings. Five remote scopes (account, profile, profile_device, profile_library, profile_series) plus client_local, and each definition declares its own resolution order rather than inheriting a global precedence.

Preferences versus restrictions. internal/policy already resolves max_playback_quality and metadata-language limits over the same controls this contract resolves preferences for. Definitions declare constrained_by; the effective response reports the permitted value alongside the user's stored one; and a mutation that exceeds a restriction is stored, not rejected — a capped 4K preference should take effect the day the cap lifts, not be destroyed by it.

Compatibility. Widening a scope, adding an enum member, or widening a range is additive and revision-tagged. Narrowing anything requires a new key. introduced_in is a manifest revision and attaches to individual enum members and scopes, not just whole definitions, so a newer client never offers a choice an older server will reject.

Rollout. One coordinated breaking release — no compatibility shim, no projection, no client fallback. After the cutover, no future setting requires coordination; manifest revisions move independently per repository. No settings version check is added to the authenticated middleware and nothing returns 426: deleting the old routes already produces the break, and a middleware gate would be more code in four repos for the same outcome, permanently coupling every endpoint to one subsystem's versioning.

Scope placement. Appearance and date/time move from account to profile scope. Account scope was an artifact of pre-profile user_settings storage; leaving it there means one household shares a theme and text size, and any non-child profile can restyle everyone else.

Read path. Batched context resolution, index requirements, a session-snapshot rule, and a no-regression benchmark gating the storage consolidation — profile_series resolution is per-item, so a season view would otherwise issue one request per episode.

Verified against

The current server, Apple, and Android implementations at the baselines in #376, re-checked against internal/api/handlers/settings.go, web/src/lib/settingsManifest.ts, internal/policy, internal/events, internal/jellycompat/handlers_displayprefs.go, internal/userdb, internal/userstore/pgstore, and docs/architecture/v1-scope.md.

Two things that check turned up and that shape the design: the unknown-key extension bag is real (keyUsesUserScope accepts any unregistered key), and v1 scope reads NOT LOCKED — so removing the legacy surface needs no amendment if it lands before lock.

make verify-local-paths passes.

Implementation status

Step PR
Canonical manifest and validator #473
Web appearance cache ownership (this repo, see linked PR)
Android contract drift Silo-Server/silo-android#101
Apple audio language scope Silo-Server/silo-apple#103

Supersedes #377, which was closed when its branch was renamed.

Related to #376. Does not close the implementation issue.

AI-use disclosure

Drafted and revised with AI assistance (Claude Opus 5) under maintainer direction. Every claim about current behavior was verified against the cited source files.

Quick104 and others added 5 commits July 10, 2026 21:12
Part of #376. Amends the draft in PR #377 per review:

- Scope the post-cutover hard gate to the protocol version only;
  manifest revisions become monotone-compatible within protocol 1
  (clients hide definitions newer than the connected server), so
  additive settings no longer require four-platform lockstep releases.
  Acknowledge the store-auto-update vs operator-upgrade window.
- Add the Jellyfin compatibility surface section: jellycompat is
  exempt from the handshake, DisplayPreferences seed repoints to the
  canonical resolver, and its blobs move off the legacy user_settings
  store before that table is dropped.
- Correct the foreign-key claim: delete behavior is application-
  enforced (SQLite store has no FKs; Postgres has none on
  library/series/device columns) and covered by the conformance suite.
- Soften migration failure semantics: recognized-but-unnormalizable
  values are quarantined to the rejects table; hard abort is reserved
  for structural failures. Define per-user SQLite migration atomicity.
- Specify account-scope authorization (child profiles cannot mutate),
  last-write-wins concurrency, outbox hold behavior on 426, RFC 8785
  canonical JSON for the manifest digest, delivery of settings-changed
  events over the existing internal/events hub, and note the silo-apple
  baseline branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Revises the cross-platform settings contract design after review against the
current server, web, and policy implementations. The contract model itself —
server-owned manifest, typed values, per-definition resolution order, generated
bindings, durable client outbox — is unchanged. What changes is how it ships and
what it reconciles with.

Rollout. The hard `426 Upgrade Required` gate is withdrawn. It sat on the whole
first-party authenticated chain, so a settings-version mismatch made the entire
app unusable, and app-store auto-update against self-hosted servers guarantees
that window happens. It was also version sniffing, which docs/architecture/
v1-scope.md rules against in favor of capability endpoints, and it contradicted
the coexistence approach the playback protocol v3 plan chose for a larger
cross-platform change. Detection now uses the manifest endpoint; older clients
fall back to the legacy string API through a projection over the canonical
resolver instead of being blocked.

Staging. The design is split into three independently shippable stages: A binds
the manifest to existing storage (additive, fixes all of #376's P1s, no data
moves), B consolidates storage behind an unchanged API, C retires the legacy
surface. Nothing requires a lockstep four-repository release, which also removes
the incentive to route around the contract via unregistered local.* keys.

Policy seam. internal/policy already resolves max_playback_quality and metadata
language restrictions over the same controls this contract resolves preferences
for. Adds `constrained_by`, permitted-value reporting, and the rule that a
restriction filters what a preference does rather than validating what it is, so
a capped profile never renders a choice playback will refuse.

Also: revision-tagged enum members, scopes, and bounds so widening stays safe
where the previous blanket immutability rule was both too strict (scope
widening) and too weak (defaults and enum widening were already mutable);
appearance and date/time moved from account to profile scope, since account
scope was an artifact of pre-profile storage and let any non-child profile
restyle the household; account writes narrowed to the primary profile; a read
path section with a batched context endpoint, index requirements, and a
no-regression gate on stage B; degraded settings mode instead of account
lockout on a quarantined per-user SQLite database; a separate user_settings
event channel; an idempotency sweeper; jellycompat seeding pinned to profile
scope; and clearer device-scope UX copy.

Related to #376. Still design-only; no runtime behavior changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverts the staged A/B/C rollout from the previous commit. Phasing required a
compatibility projection of the old string API over the new resolver, plus
manifest bindings to the very tables the migration replaces — both written only
to be deleted, and a meaningful fraction of the permanent code in this
subsystem. The project is pre-1.0, v1 scope is unlocked, and data volumes are
small, so one clean switchover costs less than the scaffolding needed to avoid
it. Server, bundled web, Apple, and Android now update together, and mixed
version operation is an explicit non-goal.

No client fallback either. A new client against a pre-contract server shows
server-upgrade-required rather than degrading to local defaults; there is no
legacy write path.

The 426 middleware gate stays withdrawn, on narrower grounds than before. It is
not about giving mismatched clients grace — it is that deleting the routes
already produces the break, so the gate is strictly more code in four repos for
the same outcome, and it permanently couples every endpoint in the product to
one subsystem's versioning for a one-time event. Route removal has no such tail.
An old client gets 404 on removed routes; that is what "update both" means.

Kept from the staging pass, since it costs nothing: the migration does not DROP
superseded columns. It stops reading them and leaves them for a trivial
follow-up migration one release later, which turns recovery from "restore the
backup and the prior binary" into "revert the binary". Nothing reads them and no
client can reach them, so this is an operator affordance, not a compatibility
path.

Also adds a phase that ships the #376 P1 fixes ahead of the contract — the Apple
audio-language no-op, Android's key/clamp/default drift, the durable outboxes,
and web cache owner-tagging are all independent of the manifest and should not
wait on the migration. Notes the release-ordering hazard found while checking
this: silo-android publishes plain versions straight to Play Store while
silo-server has no versioned releases at all, only Docker latest, so operators
have no version number to reason about.

Unchanged from the prior revision: the policy seam and constrained_by, the
widening-vs-narrowing compatibility rules and revision-tagged sub-elements,
appearance and date/time at profile scope, the read path section, degraded
settings mode on a quarantined user database, the separate user_settings event
channel, the idempotency sweeper, jellycompat seeding at profile scope, and the
device-scope UX copy. Those are orthogonal to rollout.

Related to #376. Still design-only; no runtime behavior changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Coordinated server/client updates are expected during beta, so the pre-release
gate no longer frames the Play Store / Docker latest ordering as a hazard. Keeps
the actionable part: release notes name the server build to pull alongside the
client versions, since image tags do not carry that pairing.

Related to #376.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fce58648-ecca-4868-9575-0b48c38bfc93

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/settings-contract-design

Comment @coderabbitai help to get the list of available commands.

@Quick104

Copy link
Copy Markdown
Contributor Author

Superseded by #479, which combines the design, the manifest, the ui.theme scope decision, and the web appearance cache fix into a single server PR. Same four commits, unchanged.

@Quick104 Quick104 closed this Jul 25, 2026
@Quick104
Quick104 deleted the docs/settings-contract-design branch July 25, 2026 19:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant