Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions docs/early-access/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ without PII and never stolen.
## Data ownership

The schema compatibility barrel remains `lib/db/schema.ts`; domain definitions
move below `lib/db/schema/`. Append migration0007, never rewrite0000–0006.
move below `lib/db/schema/`. Never rewrite `0000`–`0004`. The unapplied tail is
squashed as `0005_early_access_foundation`.

- Identity: users/authIdentities/userEmails; externalUserId becomes nullable
legacy compatibility data. No new identity rows need a subject-derived alias.
Expand Down Expand Up @@ -154,9 +155,9 @@ the obsolete guard. Existing production identifiers and token formats stay intac

## Contract amendment 3 — single-use OAuth code redemption

Independent review found inherited replayable authorization codes. Add migration
0008 with `oauth_code_redemptions`: unique SHA-256 code digest, expiration and
creation timestamps. No raw credentials are stored. After signature, PKCE and
Independent review found inherited replayable authorization codes. The squashed
`0005_early_access_foundation` migration includes `oauth_code_redemptions`:
unique SHA-256 code digest, expiration and creation timestamps. No raw credentials are stored. After signature, PKCE and
approval validation, `consumeAuthorizationCode(code, expiresAt)` atomically inserts
the digest before token minting; duplicate redemption is invalid_grant and storage
failure is unavailable. A failed upstream mint still consumes the code; restart
Expand Down
2 changes: 1 addition & 1 deletion docs/early-access/migration-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Outbox counts are a snapshot, not permission to process the backlog.
`br-super-bird-auln2med` is the marked disposable test database, separate from
both the old user-test preview and the new runtime preview. Migration test builds
0000–0004 in a transaction-local synthetic schema, seeds legacy fixtures, applies
0005–0007, verifies preserved counts, admin/subscription backfill idempotency,
squashed `0005_early_access_foundation`, verifies preserved counts, admin/subscription backfill idempotency,
uniqueness/FKs, append-only audit, immutable aliases and synthetic grandfather
dry-run/repeat application/revocation rejection, then rolls back.

Expand Down
16 changes: 7 additions & 9 deletions docs/waitlist-cutover.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ the only session accepted by `/admin`.
## Database boundaries

- Migrations `0000` through `0004` reproduce the deployed waitlist schema.
- Migration `0005_canonical_user_foundation` adds `users`,
`auth_identities`, `user_emails`, and the nullable waitlist user link.
- Migration `0006_auth_identity_provider_metadata` records the Auth0 authority
and provider strategy on identities. Existing identities receive this metadata
on their next successful reconciliation.
- Migration `0005_early_access_foundation` is the unapplied tail: canonical
users (`users`, `auth_identities`, `user_emails`), identity metadata, early
access domains, OAuth code receipts, and the admin/subscription backfills.
- Production uses the existing waitlist Postgres database. Preview deployments,
CI, and destructive tests must use an isolated database branch.
- Do not copy production records. Apply `0005` in place after taking the
provider's normal point-in-time backup and before enabling Console traffic;
then apply `0006`. Neither migration rewrites waitlist contact data.
provider's normal point-in-time backup and before enabling Console traffic.
It does not rewrite waitlist contact data.

Authenticated requests keep the existing deterministic PymtHouse external user
ID. A successful Auth0 login also best-effort upserts the application-owned user,
Expand All @@ -43,15 +41,15 @@ scheduler until an explicitly configured Vercel cron replacement is tested.

## Staged release checklist

1. Deploy a preview with the isolated database and migrations `0000`-`0006`.
1. Deploy a preview with the isolated database and migrations `0000`-`0005`.
2. Verify `/waitlist`, signup delivery, `/verify`, referrals, consent changes,
newsletter sync, admin access, CSV export, and outbox retries.
3. Verify Auth0 callback and repeat-login reconciliation, including verified and
unverified email cases and an intentional database outage.
4. Smoke-test billing, keys, device approval, MCP discovery, and `/api/mcp`; the
external PymtHouse identifier must match its pre-cutover value.
5. Record production counts for signups, confirmed members, consent events, and
pending outbox events. Apply `0005` and `0006`, deploy Console, and compare the counts.
pending outbox events. Apply `0005`, deploy Console, and compare the counts.
6. Move `earlyaccess.livepeer.org` only after the deployed checks pass. Retain
the standalone waitlist deployment and its domain mapping as the rollback
target until the observation window closes.
Expand Down
44 changes: 0 additions & 44 deletions drizzle/0005_canonical_user_foundation.sql

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,50 @@
CREATE TYPE "public"."user_status" AS ENUM('active', 'disabled');--> statement-breakpoint
CREATE TABLE "auth_identities" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"provider" text NOT NULL,
"provider_subject" text NOT NULL,
"external_user_id" text NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"last_seen_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "user_emails" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"email" text NOT NULL,
"normalized_email" text NOT NULL,
"source" text NOT NULL,
"is_primary" boolean DEFAULT false NOT NULL,
"verified_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"status" "user_status" DEFAULT 'active' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
"last_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
"disabled_at" timestamp with time zone
);
--> statement-breakpoint
ALTER TABLE "waitlist_signups" ADD COLUMN "user_id" uuid;--> statement-breakpoint
ALTER TABLE "auth_identities" ADD CONSTRAINT "auth_identities_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "user_emails" ADD CONSTRAINT "user_emails_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "auth_identities_provider_subject_idx" ON "auth_identities" USING btree ("provider","provider_subject");--> statement-breakpoint
CREATE UNIQUE INDEX "auth_identities_external_user_id_idx" ON "auth_identities" USING btree ("external_user_id");--> statement-breakpoint
CREATE INDEX "auth_identities_user_idx" ON "auth_identities" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "user_emails_user_normalized_idx" ON "user_emails" USING btree ("user_id","normalized_email");--> statement-breakpoint
CREATE UNIQUE INDEX "user_emails_verified_normalized_idx" ON "user_emails" USING btree ("normalized_email") WHERE "user_emails"."verified_at" is not null;--> statement-breakpoint
CREATE UNIQUE INDEX "user_emails_primary_user_idx" ON "user_emails" USING btree ("user_id") WHERE "user_emails"."is_primary" = true;--> statement-breakpoint
CREATE INDEX "users_status_idx" ON "users" USING btree ("status");--> statement-breakpoint
ALTER TABLE "waitlist_signups" ADD CONSTRAINT "waitlist_signups_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "waitlist_signups_user_idx" ON "waitlist_signups" USING btree ("user_id") WHERE "waitlist_signups"."user_id" is not null;
--> statement-breakpoint
ALTER TABLE "auth_identities" ADD COLUMN "provider_metadata" jsonb DEFAULT '{}'::jsonb NOT NULL;
--> statement-breakpoint
CREATE TYPE "public"."access_grant_status" AS ENUM('approved', 'revoked');--> statement-breakpoint
CREATE TYPE "public"."email_subscription_status" AS ENUM('subscribed', 'unsubscribed');--> statement-breakpoint
CREATE TABLE "external_accounts" (
Expand Down Expand Up @@ -198,3 +245,9 @@ END;
$$;--> statement-breakpoint
CREATE TRIGGER external_accounts_preserve_mapping BEFORE UPDATE ON external_accounts
FOR EACH ROW EXECUTE FUNCTION preserve_external_account_mapping();
--> statement-breakpoint
CREATE TABLE "oauth_code_redemptions" (
"code_hash" text PRIMARY KEY NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
1 change: 0 additions & 1 deletion drizzle/0006_auth_identity_provider_metadata.sql

This file was deleted.

5 changes: 0 additions & 5 deletions drizzle/0008_oauth_code_redemptions.sql

This file was deleted.

Loading
Loading