Found by the step-1 measurement of #11045 (the settings bind-window gate). Filed per-plugin rather than fixed there, per that card's ruling: #11045 is the gate, not a fourth declaration.
What happens
AuthPlugin reaches getService('settings') from kernel:ready handlers it registers in start(), and it uses the handle in the same tick:
packages/plugins/plugin-auth/src/auth-plugin.ts:1207 — bindAuthSettings() resolves the handle and then await applySettings(), which calls settings.getNamespace('auth').packages/plugins/plugin-auth/src/auth-plugin.ts:728 — the second acquisition in the same plugin.
It is reached transitively, at depth 3 from one of the registrations:
ctx.hook('kernel:ready', () => runBackfill('kernel:ready')) // auth-plugin.ts:1029
→ runBackfill (a const inside start())
→ this.ensureAuthSettingsBound(ctx) // :1188
→ this.bindAuthSettings(ctx) // :1202
→ ctx.getService<SettingsReadSurface>('settings') // :1207
→ await settings.getNamespace('auth')
and at depth 2 from auth-plugin.ts:674.
SettingsServicePlugin binds its data engine from its ownstart()-registered kernel:ready hook (settings-service-plugin.ts:153). Handlers run in registration order, and registration order is start() order. AuthPlugin declares dependencies = ['com.objectstack.engine.objectql'] and nothing about settings, so nothing orders it after the settings plugin.
Why it fires on the shipped composition
packages/cli/src/commands/serve.ts does kernel.use(new AuthPlugin(...)) at line 2693; SettingsServicePlugin arrives later, from the capability loop at line ~3417. resolvePluginOrder preserves insertion order for plugins with no edge between them, so auth starts first.
Measured with the real resolvePluginOrder from packages/core/src/plugin-order.ts, fed the verbatim declarations of the five plugins involved in the serve insertion order:
start() order: ["com.objectstack.engine.objectql","com.objectstack.auth","com.objectstack.audit","com.objectstack.service.settings","com.objectstack.mcp"]
auth before settings? true
So auth's kernel:ready handlers are registered — and therefore fire — before the engine bind.
Consequence
In that window SettingsService.loadRows takes its this.memory branch, so getNamespace('auth') answers the manifest defaults with source: 'default' while the deployment's persisted sys_setting rows go unread (this is the exact class #10250 documented and made audible via reportPreBindRead). Everything applySettings() derives is therefore computed from defaults at boot: the ADR-0093 membership policy the D6 backfill runs under, and the google_* social-provider config. settings.subscribe('auth', …) only re-applies on a later change, so a workspace that configured auth in Setup and never touched it again keeps booting with the wrong values.
Note the D6 backfill's own comment at auth-plugin.ts:1175 already reasons about this ordering hazard between auth's two hooks — the hazard against the settings plugin is the one still open.
Repair
The #10250 shape — one declaration on AuthPlugin:
optionalDependencies=['com.objectstack.service.settings'];
requiresServices would not do it: that asserts the service is registered before init() (it always is), and carries no start() ordering.
Verification: after the fix, pnpm check:settings-bind-window must go green with the com.objectstack.auth entry deleted from KNOWN_PRE_BIND_READS in scripts/check-settings-bind-window.mjs — that ledger is shrink-only and errors on a stale entry, so the deletion is part of the fix, not follow-up.
Suggested lane: whoever owns packages/plugins/plugin-auth.
Found by the step-1 measurement of #11045 (the settings bind-window gate). Filed per-plugin rather than fixed there, per that card's ruling: #11045 is the gate, not a fourth declaration.
What happens
AuthPluginreachesgetService('settings')fromkernel:readyhandlers it registers instart(), and it uses the handle in the same tick:packages/plugins/plugin-auth/src/auth-plugin.ts:1207—bindAuthSettings()resolves the handle and thenawait applySettings(), which callssettings.getNamespace('auth').packages/plugins/plugin-auth/src/auth-plugin.ts:728— the second acquisition in the same plugin.It is reached transitively, at depth 3 from one of the registrations:
and at depth 2 from
auth-plugin.ts:674.SettingsServicePluginbinds its data engine from its ownstart()-registeredkernel:readyhook (settings-service-plugin.ts:153). Handlers run in registration order, and registration order isstart()order.AuthPlugindeclaresdependencies = ['com.objectstack.engine.objectql']and nothing about settings, so nothing orders it after the settings plugin.Why it fires on the shipped composition
packages/cli/src/commands/serve.tsdoeskernel.use(new AuthPlugin(...))at line 2693;SettingsServicePluginarrives later, from the capability loop at line ~3417.resolvePluginOrderpreserves insertion order for plugins with no edge between them, so auth starts first.Measured with the real
resolvePluginOrderfrompackages/core/src/plugin-order.ts, fed the verbatim declarations of the five plugins involved in theserveinsertion order:So auth's
kernel:readyhandlers are registered — and therefore fire — before the engine bind.Consequence
In that window
SettingsService.loadRowstakes itsthis.memorybranch, sogetNamespace('auth')answers the manifest defaults withsource: 'default'while the deployment's persistedsys_settingrows go unread (this is the exact class #10250 documented and made audible viareportPreBindRead). EverythingapplySettings()derives is therefore computed from defaults at boot: the ADR-0093 membership policy the D6 backfill runs under, and thegoogle_*social-provider config.settings.subscribe('auth', …)only re-applies on a later change, so a workspace that configured auth in Setup and never touched it again keeps booting with the wrong values.Note the D6 backfill's own comment at
auth-plugin.ts:1175already reasons about this ordering hazard between auth's two hooks — the hazard against the settings plugin is the one still open.Repair
The #10250 shape — one declaration on
AuthPlugin:requiresServiceswould not do it: that asserts the service is registered beforeinit()(it always is), and carries nostart()ordering.Verification: after the fix,
pnpm check:settings-bind-windowmust go green with thecom.objectstack.authentry deleted fromKNOWN_PRE_BIND_READSinscripts/check-settings-bind-window.mjs— that ledger is shrink-only and errors on a stale entry, so the deletion is part of the fix, not follow-up.Suggested lane: whoever owns
packages/plugins/plugin-auth.