Summary
On a fresh database's first boot, bootstrapMiddleware seeds system data with a flat Promise.all([...]) that races a foreign-key dependency: two steps create document_types rows while two other steps insert documents that FK-reference those rows. When a consumer insert wins the race, D1 throws FOREIGN KEY constraint failed. The per-step .catch() swallows it, so RBAC roles/verbs and every core plugin are silently left unseeded — the admin then 403s and the site runs with no roles/plugins.
Two facets compound it:
- No single-flight guard. Two concurrent requests on a cold isolate both enter bootstrap and seed in parallel, re-opening the FK window across the two runs.
- The broken state latches. Bootstrap writes an unconditional KV skip-marker (24h TTL) even when seeding failed. Every subsequent request/isolate then takes the KV fast-path, skips D1 entirely, and the site stays broken until the marker expires or is cleared.
How it was hit (real, not theoretical)
Spinning up a clean greenfield stack (fresh D1, empty KV) on beta.25, the very first request logged:
✘ [ERROR] [PluginBootstrap] Error ensuring plugin Authentication System:
Error: D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT_FOREIGNKEY
at PluginService.installPlugin (services/plugin-service.ts)
at async Promise.all (index 4)
✘ [ERROR] [Bootstrap] Error seeding RBAC documents:
Error: D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT_FOREIGNKEY
Both failing steps are inside the await Promise.all([...]) in middleware/bootstrap.ts — the document-type producers had not finished when the FK consumers ran.
Proposed fix
(1) Order the seed so the two document-type producers run and finish before the FK consumers; (2) add an in-flight single-flight promise so concurrent cold-isolate requests await one bootstrap instead of racing; (3) make the "bootstrap complete" latch + KV skip-marker conditional on success, so a failed boot self-heals on the next request instead of latching for 24h.
A PR implementing this is ready — a surgical change to middleware/bootstrap.ts plus a real-DB regression test.
Note
This depends on the beta.25 test-infra fix (separate issue/PR) only for a green unit suite — the ~25 failures on this branch are the pre-existing set fixed there, and none of them touch bootstrap.ts.
Summary
On a fresh database's first boot,
bootstrapMiddlewareseeds system data with a flatPromise.all([...])that races a foreign-key dependency: two steps createdocument_typesrows while two other steps insertdocumentsthat FK-reference those rows. When a consumer insert wins the race, D1 throwsFOREIGN KEY constraint failed. The per-step.catch()swallows it, so RBAC roles/verbs and every core plugin are silently left unseeded — the admin then 403s and the site runs with no roles/plugins.Two facets compound it:
How it was hit (real, not theoretical)
Spinning up a clean greenfield stack (fresh D1, empty KV) on beta.25, the very first request logged:
Both failing steps are inside the
await Promise.all([...])inmiddleware/bootstrap.ts— the document-type producers had not finished when the FK consumers ran.Proposed fix
(1) Order the seed so the two document-type producers run and finish before the FK consumers; (2) add an in-flight single-flight promise so concurrent cold-isolate requests await one bootstrap instead of racing; (3) make the "bootstrap complete" latch + KV skip-marker conditional on success, so a failed boot self-heals on the next request instead of latching for 24h.
A PR implementing this is ready — a surgical change to
middleware/bootstrap.tsplus a real-DB regression test.Note
This depends on the beta.25 test-infra fix (separate issue/PR) only for a green unit suite — the ~25 failures on this branch are the pre-existing set fixed there, and none of them touch
bootstrap.ts.