251 lines · 13.0 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Cloud-specific identity & multi-tenancy tables// ---------------------------------------------------------------------------//// AuthKit owns the canonical user/membership data. We mirror it locally:////   - `accounts`       — login identity + profile (foreign key anchor for//                        created_by, etc.; email/name/avatar for member lists)//   - `organizations`  — billing entity, scoping root for all domain data//   - `memberships`    — which accounts belong to which organizations, with//                        the WorkOS role and status//   - `workos_sync`    — the WorkOS Events API cursor the reconciler resumes//                        from, and the replay boundary the one-off backfill//                        records//// The mirror is fed by login (the callback has the user + memberships in// hand), write-through on every Executor-initiated change, and the WorkOS// Events API (dashboard-side changes). It is the read path for membership and// member lists — WorkOS is a write target and an event source, never a// per-request read. Invitations are NOT mirrored; they stay live in WorkOS.//// `workos_updated_at` on `accounts` and `memberships` is the WorkOS// `updatedAt` of the payload that last wrote the row. Every upsert is guarded// on it, so feeders can be replayed and reordered without an older payload// clobbering a newer one. import { sql } from "drizzle-orm";import { index, pgTable, primaryKey, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core"; /** * Login identity + mirrored WorkOS profile. The `id` is the WorkOS user ID. * Profile columns are nullable because a row can be minted by `ensureAccount` * (an api-key path, a membership arriving before its user event) with nothing * but the id; the next user payload fills them in. */export const accounts = pgTable(  "accounts",  {    id: text("id").primaryKey(),    email: text("email"),    firstName: text("first_name"),    lastName: text("last_name"),    avatarUrl: text("avatar_url"),    /** WorkOS `updatedAt` of the user payload that last wrote this row. */    workosUpdatedAt: timestamp("workos_updated_at", { withTimezone: true }),    lastSignInAt: timestamp("last_sign_in_at", { withTimezone: true }),    createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),  },  (t) => ({    // `findByEmail` and the search filter compare lower-cased; the index    // matches that expression so the lookup stays indexed.    emailLowerIdx: index("accounts_email_lower_idx").on(sql`lower(${t.email})`),  }),); /** * Organization (billing entity, scoping root). The `id` is the WorkOS * organization ID. The `slug` is OURS, not WorkOS's (their org object has no * slug): minted at the moment a row is inserted (the single mint point is * `upsertOrganization`) and stable across renames so org URLs don't break. * NOT NULL — there is no nullable window: legacy rows were backfilled once and * every insert since carries a slug. */export const organizations = pgTable(  "organizations",  {    id: text("id").primaryKey(),    name: text("name").notNull(),    slug: text("slug").notNull(),    /**     * When this organization's membership list was last FULLY scanned from     * WorkOS (the one-off backfill, or the on-demand scan a seat count     * triggers), or null if it never was. Until then the mirror may hold only     * the members login and write-through happened to record, so a count read     * from it is partial; every seat gate checks this mark first. Per     * organization, never database-wide: an org mirrored lazily after a     * backfill ran starts unmarked and is scanned on its first count. The     * mark also orders membership writes: a payload stamped before it is     * refused, since the scan was the full listing at that instant and a     * membership it did not contain was revoked before it — before the     * events replay boundary, so nothing would tombstone it again.     */    backfilledAt: timestamp("backfilled_at", { withTimezone: true }),    /**     * When this organization was deleted, or null while it is live. Set FIRST     * by cloud's own deletion flow (`auth/handlers.ts` deleteOrganization),     * before the WorkOS delete and the local purge, and by the     * `organization.deleted` event for an org deleted in the WorkOS dashboard     * — which MINTS the row as a tombstone when the mirror has never seen the     * organization: membership is authorized from the local mirror, so a     * marked organization refuses every session at once, whether or not the     * later steps land. Membership rows are left as they are until the purge     * (so the admin who started the deletion can retry it after a step     * failed), and the purge (`db/org-deletion.ts`) removes them but KEEPS     * this row as a tombstone: a feeder that fetched a membership before the     * deletion and writes it after (a login that stalled across the deletion)     * finds the tombstone and does not mint the organization live. A marked     * organization is never renamed.     */    deletedAt: timestamp("deleted_at", { withTimezone: true }),    /**     * The instant the stored `name` is known to have been the organization's     * name in WorkOS: the WorkOS `updatedAt` of the organization payload that     * wrote it, or — for a name learned from a membership list at sign-in,     * which carries no organization timestamp — the instant that list was     * fetched. A name write stamped earlier than this is refused     * (`upsertOrganization`), so a sign-in whose list predates a rename cannot     * revert it. Null only on rows written before the stamp existed.     */    workosUpdatedAt: timestamp("workos_updated_at", { withTimezone: true }),    createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),  },  (t) => ({    slugUnique: uniqueIndex("organizations_slug_unique").on(t.slug),  }),); /** * Account ↔ organization link, mirroring the WorkOS organization membership. * Answers "which workspaces does this account belong to?" and "is this caller * an active member with which role?" without a WorkOS round-trip, and gives * per-(account, organization) data a foreign key to point at. * * `membershipId` is the WorkOS `om_…` id — nullable only because rows written * before the mirror existed carry none; every feeder sets it. `role` is the * WorkOS role slug as issued (`admin` / `member`); `status` is the WorkOS * membership status (`active` / `pending` / `inactive`). A membership deleted * in WorkOS is never dropped here: it is tombstoned as `inactive` with * `deleted_at` set, so a feeder replaying an older payload cannot resurrect it. * * `deleted_at` means "the membership under `membership_id` was DELETED in * WorkOS" — an identity fact, not a timestamp to order payloads by. WorkOS * never reuses a deleted `om_…` id, so any payload naming that id is stale * however it is stamped, and a payload naming a DIFFERENT id for the same * (account, organization) is the member re-added: a replacement, ordered by * `workos_updated_at` like every other write. Distinct from `status = * 'inactive'` with `deleted_at` null, which is a membership WorkOS * deactivated but still holds and can reactivate under the same id. */export const memberships = pgTable(  "memberships",  {    accountId: text("account_id")      .notNull()      .references(() => accounts.id, { onDelete: "cascade" }),    organizationId: text("organization_id")      .notNull()      .references(() => organizations.id, { onDelete: "cascade" }),    membershipId: text("membership_id"),    role: text("role").notNull().default("member"),    status: text("status", { enum: ["active", "pending", "inactive"] })      .notNull()      .default("active"),    /** WorkOS `updatedAt` of the membership payload that last wrote this row. */    workosUpdatedAt: timestamp("workos_updated_at", { withTimezone: true }),    /**     * When the membership under `membership_id` was deleted in WorkOS, or null     * while WorkOS still holds it (whatever its `status`). Set once; cleared     * only when a replacement membership (another id) takes the row over.     */    deletedAt: timestamp("deleted_at", { withTimezone: true }),    createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),  },  (t) => ({    pk: primaryKey({ columns: [t.accountId, t.organizationId] }),    membershipIdUnique: uniqueIndex("memberships_membership_id_unique").on(t.membershipId),    organizationIdx: index("memberships_organization_id_idx").on(t.organizationId),  }),); /** * Every WorkOS membership id (`om_…`) a DELETE has named, keyed by that id * alone. The `memberships` row is keyed by (account, organization) and holds * ONE membership id, so a row-level tombstone can only record the deletion of * the id the row happens to carry: a delete of membership B arriving while * the row still holds an older membership A of the same pair (A replaced by * B in WorkOS before the mirror saw either, B then deleted) has no row to * tombstone, and a later payload of B — stamped after A, under another id — * would take the row over live. This ledger is what the mirror consults * instead: a delete always records the id here, whatever the row holds, and * no membership write ever names a recorded id again, however it is stamped * — WorkOS never reuses a deleted `om_…` id. `deleted_at` records when; it * orders nothing. Rows cascade with their account and organization. */export const membershipTombstones = pgTable(  "membership_tombstones",  {    membershipId: text("membership_id").primaryKey(),    accountId: text("account_id")      .notNull()      .references(() => accounts.id, { onDelete: "cascade" }),    organizationId: text("organization_id")      .notNull()      .references(() => organizations.id, { onDelete: "cascade" }),    deletedAt: timestamp("deleted_at", { withTimezone: true }).notNull().defaultNow(),  },  (t) => ({    organizationIdx: index("membership_tombstones_organization_id_idx").on(t.organizationId),  }),); /** * The WorkOS Events API sync state. One row per stream (`id` names the * stream; the reconciler uses `"events"`), holding the id of the last event * applied. Advanced only by compare-and-set, so two concurrent reconciler * runs cannot both believe they own the stream: the loser's CAS fails and it * stops. * * `range_start` on the `"events"` row is the REPLAY BOUNDARY: the instant the * FIRST one-off backfill run (`scripts/backfill-workos-mirror.ts`) began * reading WorkOS, recorded BEFORE its first listing. Everything before it is * covered by that backfill; the reconciler's first run (no cursor yet) reads * the events stream from here, so a revocation between the backfill and the * first run is never skipped. Written once: a run that fails part-way leaves * it standing and its retry keeps it, and a later run keeps it too, because * the backfill does not refresh everything the events stream carries * (organization renames, deleted users' profiles) — those after the first * boundary are replayed from it. Without a cursor or a boundary the * reconciler does not guess; it waits for the backfill. * * `backfill_completed_at` is when a backfill run first wrote EVERY live * organization (`scripts/backfill-workos-mirror.ts` completing, or refusing * an organization only because a later listing was already applied). Until * it is set, the mirror may lack members who have not signed in since it * shipped, so a membership check read from it would deny them: it is the * first half of the mirror-readiness mark the authorization path consults * before trusting the mirror over WorkOS. Write-once — a later completed run * keeps the first instant, so readiness never flips back. Per-organization * completeness for the seat gates is tracked separately * (`organizations.backfilled_at`). * * `drained_at` is when a reconciler run last read the events stream to its * END (an empty page, or a page with nothing after it) — the second half of * the readiness mark: a mirror whose reconciler has not caught up recently * may still grant a member WorkOS already revoked, so the authorization * path trusts the mirror only while this is within its lag budget. Moved * forward by every draining run; never cleared. A run that stops at its * page budget or yields to another run leaves it as it was. * * Migration 0019 seeds the boundary and the completion mark on a database * with no organizations, where there is nothing to backfill; `drained_at` * is left for the reconciler's first run to set (migration 0020). */export const workosSync = pgTable("workos_sync", {  id: text("id").primaryKey(),  cursor: text("cursor"),  rangeStart: timestamp("range_start", { withTimezone: true }),  backfillCompletedAt: timestamp("backfill_completed_at", { withTimezone: true }),  drainedAt: timestamp("drained_at", { withTimezone: true }),  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),});