@@ -19,9 +19,9 @@ each managed independently.
1919
2020** Reads are the hot path.** A user's effective configuration for a
2121` config_name ` is the ** deep merge of every fragment that applies to
22- them** , taken
23- straight from ` app_config_fragments ` in ` rank ` order — a single-table
24- query with ** no joins and no permission lookup** .
22+ them** , taken from ` app_config_fragments ` ordered by the ` rank ` of each
23+ fragment's allow-list entry — one indexed join, ** no permission
24+ lookup** .
2525
2626** Write authorization is set up ahead of time.** Every config name is
2727** explicitly registered** in ` app_config_definitions ` , and ` app_config_allow_list `
@@ -68,18 +68,20 @@ Three scopes cover the use cases (`public` for the pre-login shell):
6868- ** Explicitly registered names.** Every config name lives in
6969 ` app_config_definitions ` ; fragments and allow-list entries reference it by
7070 foreign key. No fragment may exist for an unregistered ` config_name ` .
71- - ** Reads are join-free and unconditional.** The merge reads
72- ` app_config_fragments ` alone, ordering the existing fragments by
73- ` rank ` . No allow-list or policy is consulted at read time — the hot
74- path stays a single indexed scan .
71+ - ** Reads are unconditional.** The merge reads ` app_config_fragments `
72+ joined to ` app_config_allow_list ` only for each fragment's ` rank ` — an
73+ indexed ` (config_name, scope_type) ` join . No permission or policy is
74+ evaluated at read time .
7575- ** Allow-list = the write gate for every fragment.** ` app_config_allow_list `
7676 holds ** one record per ` (config_name, scope_type) ` ** ; a fragment at
7777 that scope may be created/updated/purged ** only if** the record exists
7878 — through the admin mutations and the regular ones alike. What sets
7979 admins apart is that they alone manage the allow-list (and the
8080 ` app_config_definitions ` ) itself. It governs ** writes only** — never reads.
81- - ** ` rank ` lives on the fragment.** A fragment's ` rank ` is its merge
82- priority within a ` config_name ` ; the read merge orders fragments by it.
81+ - ** ` rank ` lives on the allow-list entry.** Merge priority is an
82+ admin-owned policy: it sits on the (admin-managed) allow-list entry,
83+ not on the fragment — a fragment owner editing their own fragment can
84+ never re-order the merge (see §2).
8385- ** Single source-of-truth table.** One ` app_config_fragments ` table
8486 holds every scope; only the exposure layer is split.
8587
@@ -109,10 +111,11 @@ Keyed by the natural composite `(scope_type, scope_id, config_name)`
109111- ` scope_id ` — the scope's identifier (see convention below).
110112- ` config_name ` — FK → ` app_config_definitions.config_name ` .
111113- ` config ` — schema-less JSON payload.
112- - ` rank ` — integer merge priority within the ` config_name ` (low → high;
113- higher wins). Assigned on create (see §2).
114114- ` created_at ` / ` updated_at ` .
115115
116+ The fragment carries no rank of its own — its merge priority is its
117+ allow-list entry's ` rank ` (see §2).
118+
116119### ` app_config_allow_list ` — the per-` (config_name, scope_type) ` write gate
117120
118121One row per ` (config_name, scope_type) ` (unique) — a normalized,
@@ -125,12 +128,14 @@ advance.
125128 (` public | domain | user ` ). A user-overridable config carries a
126129 ` (config_name, user) ` row; an admin-only value carries
127130 ` (config_name, domain) ` and/or ` (config_name, public) ` .
131+ - ` rank ` — the merge priority every fragment under this entry carries
132+ (low → high; higher wins). Defaults per scope type (see §2); admins
133+ may set it explicitly.
128134- ` created_at ` / ` updated_at ` .
129135
130- A row's ** presence** is the entire signal — there is no ` rank ` here,
131- because the allow-list never participates in the merge. It gates ** both**
132- write paths; admins, unlike users, may also create/update/purge the
133- allow-list rows themselves.
136+ A row's ** presence** is the write grant, and its ` rank ` is the merge
137+ order of its fragments. It gates ** both** write paths; admins, unlike
138+ users, may also create/update/purge the allow-list rows themselves.
134139
135140### Scope-ID convention
136141
@@ -185,7 +190,8 @@ boundary.
185190 merged value is the admin's (` public ` / ` domain ` fragments only).
186191- ** Overridable** : the admin grants ` (config_name, user) ` . The admin
187192 sets the ` domain ` default; the user freely creates/updates/purges
188- their own ` user ` fragment, which (higher ` rank ` ) wins on merge.
193+ their own ` user ` fragment, which wins on merge (the ` user ` entry's
194+ default ` rank ` is the highest).
189195- ** User-only** : the grant exists and no admin fragment is published.
190196
191197To promote a fixed value to user-customizable, the admin adds a single
@@ -198,19 +204,25 @@ is already stored).
198204
199205## 2. ` rank ` — merge priority
200206
201- ` rank ` is the integer priority a ** fragment** carries within a
202- ` config_name ` ; the read merge applies fragments in ` rank ` order (low →
203- high, higher wins).
204-
205- - ** Assignment.** A new fragment is placed after the existing ones for
206- the same ` config_name ` , so a later-created fragment outranks earlier ones by
207- default. Admins re-order by setting ` rank ` explicitly. (Publishing the
208- ` domain ` default before a user writes their own copy naturally gives
209- the ` user ` fragment the higher rank, so the user value wins.)
210- - ** No tier defaults.** Priority is the fragment's ` rank ` , not derived
211- from ` scope_type ` — a ` user ` fragment outranks a ` domain ` fragment
212- only because its ` rank ` is higher, not because ` user ` is "above"
213- ` domain ` .
207+ ` rank ` is the integer priority an ** allow-list entry** carries; every
208+ fragment written under the entry merges at that priority (low → high,
209+ higher wins). It lives on the allow-list — not the fragment — because
210+ merge order is admin policy: fragment writes are partly user-owned, and
211+ a rank on the fragment would let a user re-order the merge by editing
212+ their own row.
213+
214+ - ** Scope-type defaults.** A new entry defaults its ` rank ` from its
215+ ` scope_type ` : ` public ` = 100, ` domain ` = 200, ` user ` = 300. The
216+ defaults order the scopes so a user's own fragment overrides the
217+ domain default, which overrides the public value; the 100 gap leaves
218+ room for custom placement in between.
219+ - ** Admin override.** The admin may set ` rank ` explicitly when creating
220+ the entry — e.g. a ` domain ` entry ranked above ` user ` yields a
221+ domain-enforced value that user fragments cannot beat even where a
222+ user grant exists.
223+ - ** Per-caller totality.** For one caller and one ` config_name ` , at most
224+ one fragment applies per scope type (§3), so the entry ranks totally
225+ order every merge input.
214226
215227---
216228
@@ -219,7 +231,7 @@ high, higher wins).
219231Two read shapes exist:
220232
221233- ** ` AppConfigFragment ` ** — one raw row, regardless of scope. Carries
222- ` scopeType ` , ` scopeId ` , ` configName ` , ` rank ` , and ` config ` . Callers
234+ ` scopeType ` , ` scopeId ` , ` configName ` , and ` config ` . Callers
223235 disambiguate scope by reading ` scopeType ` (no per-scope wrapper
224236 types).
225237- ** ` AppConfig ` ** — the merged per-user view for a ` config_name ` : the
@@ -235,10 +247,10 @@ those whose scope applies to them:
235247- the user's ` user ` fragment (` scope_id = the user's id ` ).
236248
237249A single ` app_config_fragments ` query selects exactly those rows (the
238- user's domain is known from the session — ** no join, no allow-list
239- lookup ** ), orders them by ` rank ` (low → high), and deep-merges: nested
240- objects recurse, scalars and lists are wholesale-replaced, and the
241- higher ` rank ` wins on conflict.
250+ user's domain is known from the session — no permission check), joins
251+ each to its allow-list entry for the ` rank ` , orders by it (low → high),
252+ and deep-merges: nested objects recurse, scalars and lists are
253+ wholesale-replaced, and the higher ` rank ` wins on conflict.
242254
243255** Null projection.** A stored ` config ` of ` {} ` reads back as ` null ` , and
244256a merged ` config ` that is empty after combining every fragment is
@@ -276,7 +288,8 @@ likewise `null` — clients fall back to their built-in defaults.
276288 be written only after its scope's row exists.
277289- ** Pre-login public config** — anonymous read of ` public ` ` theme ` .
278290- ** Bootstrap after login** — read merged ` AppConfig ` s in one round of
279- queries; each is a single-table rank merge, no per-scope stitching.
291+ queries; each is a rank merge over the fragments joined to their
292+ allow-list entries, no per-scope stitching.
280293- ** User edits a config** — a regular create/update/purge on the
281294 caller's own ` user ` row (only where the grant exists); the response is
282295 the recomputed merge.
@@ -289,7 +302,8 @@ likewise `null` — clients fall back to their built-in defaults.
289302- ** Admin locks a value back down** — remove the ` (config_name, user) `
290303 grant ** and** purge existing ` user ` fragments (the grant gates writes,
291304 not reads).
292- - ** Admin reorders contributions** — adjust fragment ` rank ` s.
305+ - ** Admin reorders contributions** — set the allow-list entries'
306+ ` rank ` s (per ` (config_name, scope_type) ` , not per fragment).
293307- ** Admin retires a config name** — purge the fragments, then the
294308 allow-list entries, then the ` app_config_definitions ` row (purge is rejected
295309 while references remain).
0 commit comments