docs(settings): define the cross-platform settings contract - #477
Closed
Quick104 wants to merge 5 commits into
Closed
docs(settings): define the cross-platform settings contract#477Quick104 wants to merge 5 commits into
Quick104 wants to merge 5 commits into
Conversation
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>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
This was referenced Jul 25, 2026
Contributor
Author
|
Superseded by #479, which combines the design, the manifest, the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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) plusclient_local, and each definition declares its own resolution order rather than inheriting a global precedence.Preferences versus restrictions.
internal/policyalready resolvesmax_playback_qualityand metadata-language limits over the same controls this contract resolves preferences for. Definitions declareconstrained_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_inis 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_settingsstorage; 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_seriesresolution 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, anddocs/architecture/v1-scope.md.Two things that check turned up and that shape the design: the unknown-key extension bag is real (
keyUsesUserScopeaccepts 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-pathspasses.Implementation status
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.