-- Migration 009: invitations — an email-based invite to join Tapir (Stage-1 -- onboarding gate). Mathias mints one with `tapir invite `; the recipient -- visits /invite/{token}, sets a password, and Tapir creates their Dex account. -- -- Deliberately NOT user-owned and NOT under RLS: an invitation exists BEFORE the -- user does, so there is no user_id to scope by and no authenticated user context -- when the invite is created (host CLI) or consumed (public /invite handler, no -- Dex session). The token itself is the capability — a 32-byte crypto-random, -- single-use, time-boxed secret. Hence no `user_id` FK and no ENABLE/FORCE ROW -- LEVEL SECURITY here (unlike every user-owned table in migrations 003/005). CREATE TABLE invitations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email TEXT NOT NULL, token TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ ); -- Lookups are by token (both the claim and the form preview); the UNIQUE -- constraint already creates an index, this names one explicitly for clarity. CREATE INDEX idx_invitations_token ON invitations(token);