-- 990_beta_invites.sql -- v1.0.10 polish (Cluster 3.4) — soft-launch beta cohort tracking. -- -- Records each individual invitation sent for the v2.0.0 soft-launch -- bêta. Tracks (a) the invite code used in the registration link, -- (b) when the recipient redeemed it (NULL until redemption), and -- (c) which cohort segment (creator / listener / community-member / -- press) the recipient belongs to so the post-launch report can -- attribute feedback by audience. -- -- The associated email template + send script live at -- scripts/soft-launch/send-invitations.sh and reference this table -- via INSERT … RETURNING code. -- -- Privacy : the email column is the only PII here ; no behavioural -- data is stored. used_at is the redemption signal. After v2.0.0 -- public launch, run the cleanup migration in 991 (TBD) to anonymise -- the email column for invites that haven't been redeemed in 30+ days. CREATE TABLE IF NOT EXISTS public.beta_invites ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- The invite code is what the recipient pastes into the signup -- form. 16 random characters from a base32 alphabet (no 0/1/I/L -- to avoid eyestrain). Generated by send-invitations.sh. code VARCHAR(32) NOT NULL UNIQUE, email VARCHAR(320) NOT NULL, -- Free-text label so the cohort generator can carry whatever -- segmentation the operator wants (e.g. "creator-vinyl-pressing", -- "listener-jazz-mailing-list", "press-pitchfork"). Index below -- is for the post-launch report grouping. cohort VARCHAR(64) NOT NULL, -- NULL until the recipient signs up. Set by the auth handler -- when /auth/register is hit with a valid invite code. used_at TIMESTAMPTZ, -- Hard expiry so unredeemed invites can't accumulate forever. -- Default 30 days from creation ; soft-launch is short-window. expires_at TIMESTAMPTZ NOT NULL DEFAULT (NOW() + INTERVAL '30 days'), -- Operator who sent the invite — useful when reconciling "who -- gave their friend a code" during the audit. sent_by UUID REFERENCES public.users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); COMMENT ON TABLE public.beta_invites IS 'v2.0.0 soft-launch beta invitation tracking. v1.0.10 Cluster 3.4.'; COMMENT ON COLUMN public.beta_invites.code IS '16-char base32 invite code (no 0/1/I/L). Pasted into signup form.'; COMMENT ON COLUMN public.beta_invites.cohort IS 'Free-text cohort label (creator-* / listener-* / press-* / etc.).'; COMMENT ON COLUMN public.beta_invites.used_at IS 'Redemption timestamp. NULL means the invite is still pending.'; -- Lookup by code (signup path) — every /auth/register call reads it. CREATE UNIQUE INDEX IF NOT EXISTS idx_beta_invites_code ON public.beta_invites(code); -- Cohort grouping for the post-launch attribution query. CREATE INDEX IF NOT EXISTS idx_beta_invites_cohort ON public.beta_invites(cohort); -- Pending-invitations sweep — cron job that expires unused invites -- after expires_at. Partial index keeps it small. CREATE INDEX IF NOT EXISTS idx_beta_invites_pending_expiry ON public.beta_invites(expires_at) WHERE used_at IS NULL;