The org-slug column was nullable and the runtime self-healed it: every resolveOrganization (and the SSR gate) lazily minted a slug for any row that lacked one. That left a permanent nullable state and a heal-on-read cost. Move slug generation to the single moment a row is inserted — upsertOrganization mints the slug at INSERT (ON CONFLICT DO NOTHING absorbs the id/slug races, retrying with a fresh candidate) and keeps the existing slug on conflict. The three insert sites (createOrganization, acceptInvitation, the WorkOS-fallback upsert in resolveOrganization) all get a slug for free; resolveOrganization and the SSR gate's organizationDisplay no longer heal. The schema column is now NOT NULL (migration 0004), backfilled once for legacy rows. Deploy order matters and is documented in the backfill script: backfill → deploy this code → apply 0004. The old code inserts a NULL slug then heals, so NOT NULL cannot land while it still serves. Also adds e2e/cloud/auth-routing-flow.test.ts: one browser session walking the whole arc — signed out → /login → hosted-AuthKit sign-in → org-less onboarding → create org → canonical /<slug> dashboard — through the real dev-server SSR gate. The hops are covered piecewise elsewhere; this is the narrative regression guard and the only test pinning the bare-/ → /<slug>/ dashboard canonicalization a fresh login lands on.
Showing the changes to apps/cloud/scripts (1 of 13 files). Show all
Unified · Split
| @@ -1,16 +1,19 @@ | ||
| 1 | 1 | /* oxlint-disable executor/no-try-catch-or-throw -- boundary: out-of-band migration script over a raw postgres connection */ |
| 2 | 2 | // --------------------------------------------------------------------------- |
| 3 | 3 | // One-off data backfill: mint URL slugs for organizations that predate the |
| 4 | −// slug column. Run OUT-OF-BAND against the database after applying the | |
| 5 | −// schema migration that adds `organizations.slug` (0002): | |
| 4 | +// slug column (added by migration 0003, nullable). | |
| 6 | 5 | // |
| 7 | 6 | // bun run db:backfill-org-slugs:prod # op run --env-file=.env.production |
| 8 | 7 | // bun run db:backfill-org-slugs:dev # against the local PGlite dev db |
| 9 | 8 | // |
| 10 | −// Idempotent — already-slugged rows are skipped, so re-running is safe. The | |
| 11 | −// runtime also lazily mints slugs on first resolution (see | |
| 12 | −// auth/organization.ts), so this backfill is belt-and-braces for bulk | |
| 13 | −// pre-warming rather than a deploy gate. | |
| 9 | +// Idempotent — already-slugged rows are skipped, so re-running is safe. | |
| 10 | +// | |
| 11 | +// Ordering matters: the runtime now mints a slug at the moment a row is | |
| 12 | +// inserted and NO LONGER self-heals null slugs, and migration 0004 makes the | |
| 13 | +// column NOT NULL. So this backfill is the SOLE way pre-existing null-slug rows | |
| 14 | +// get a slug, and it MUST run BEFORE 0004 is applied (a null row would fail the | |
| 15 | +// NOT NULL alter). The correct sequence is: backfill (this script) → deploy the | |
| 16 | +// mint-at-insert code → apply 0004. | |
| 14 | 17 | // Pass --dry-run to print the plan without writing. |
| 15 | 18 | // --------------------------------------------------------------------------- |
| 16 | 19 | |
| @@ -47,8 +50,8 @@ for (const row of rows) { · 30 unmodified lines | ||
| 47 | 50 | planned.add(slug); |
| 48 | 51 | console.log(`${row.id} ${JSON.stringify(row.name)} -> ${slug}`); |
| 49 | 52 | if (dryRun) continue; |
| 50 | − // Guard WHERE slug IS NULL: a concurrently-deployed runtime may have lazily | |
| 51 | − // minted this org's slug between our read and this write. | |
| 53 | + // Guard WHERE slug IS NULL keeps re-runs idempotent (and tolerates a row that | |
| 54 | + // gained a slug between our read and this write). | |
| 52 | 55 | await sql`UPDATE organizations SET slug = ${slug} WHERE id = ${row.id} AND slug IS NULL`; |
| 53 | 56 | updated += 1; |
| 54 | 57 | } |