-- Migration 015: SaaS Onboarding + Auth (Stripe, Refresh Tokens, MFA, SSO, Passkeys) -- Adds tables for refresh tokens, stripe event tracking, invite tokens, -- onboarding progress, and WebAuthn passkeys. -- ============================================================================ -- 1. Modify shared.organizations — add Stripe billing columns -- ============================================================================ ALTER TABLE shared.organizations ADD COLUMN IF NOT EXISTS stripe_customer_id VARCHAR(255) UNIQUE; ALTER TABLE shared.organizations ADD COLUMN IF NOT EXISTS stripe_subscription_id VARCHAR(255) UNIQUE; ALTER TABLE shared.organizations ADD COLUMN IF NOT EXISTS trial_ends_at TIMESTAMPTZ; -- Update plan_level CHECK constraint to include new SaaS plan tiers -- (Drop and re-add since ALTER CHECK is not supported in PG) ALTER TABLE shared.organizations DROP CONSTRAINT IF EXISTS organizations_plan_level_check; ALTER TABLE shared.organizations ADD CONSTRAINT organizations_plan_level_check CHECK (plan_level IN ('standard', 'premium', 'enterprise', 'starter', 'professional')); -- ============================================================================ -- 2. New table: shared.refresh_tokens -- ============================================================================ CREATE TABLE IF NOT EXISTS shared.refresh_tokens ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES shared.users(id) ON DELETE CASCADE, token_hash VARCHAR(255) UNIQUE NOT NULL, expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user ON shared.refresh_tokens(user_id); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON shared.refresh_tokens(token_hash); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires ON shared.refresh_tokens(expires_at); -- ============================================================================ -- 3. New table: shared.stripe_events (idempotency for webhook processing) -- ============================================================================ CREATE TABLE IF NOT EXISTS shared.stripe_events ( id VARCHAR(255) PRIMARY KEY, type VARCHAR(100) NOT NULL, processed_at TIMESTAMPTZ DEFAULT NOW(), payload JSONB ); -- ============================================================================ -- 4. New table: shared.invite_tokens (magic link activation) -- ============================================================================ CREATE TABLE IF NOT EXISTS shared.invite_tokens ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID NOT NULL REFERENCES shared.organizations(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES shared.users(id) ON DELETE CASCADE, token_hash VARCHAR(255) UNIQUE NOT NULL, expires_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_invite_tokens_hash ON shared.invite_tokens(token_hash); CREATE INDEX IF NOT EXISTS idx_invite_tokens_user ON shared.invite_tokens(user_id); -- ============================================================================ -- 5. New table: shared.onboarding_progress -- ============================================================================ CREATE TABLE IF NOT EXISTS shared.onboarding_progress ( organization_id UUID PRIMARY KEY REFERENCES shared.organizations(id) ON DELETE CASCADE, completed_steps TEXT[] DEFAULT '{}', completed_at TIMESTAMPTZ, updated_at TIMESTAMPTZ DEFAULT NOW() ); -- ============================================================================ -- 6. New table: shared.user_passkeys (WebAuthn) -- ============================================================================ CREATE TABLE IF NOT EXISTS shared.user_passkeys ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES shared.users(id) ON DELETE CASCADE, credential_id TEXT UNIQUE NOT NULL, public_key TEXT NOT NULL, counter BIGINT DEFAULT 0, device_name VARCHAR(255), transports TEXT[], created_at TIMESTAMPTZ DEFAULT NOW(), last_used_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_user_passkeys_user ON shared.user_passkeys(user_id); CREATE INDEX IF NOT EXISTS idx_user_passkeys_cred ON shared.user_passkeys(credential_id); -- ============================================================================ -- 7. Modify shared.users — add MFA/WebAuthn columns -- ============================================================================ ALTER TABLE shared.users ADD COLUMN IF NOT EXISTS totp_verified_at TIMESTAMPTZ; ALTER TABLE shared.users ADD COLUMN IF NOT EXISTS recovery_codes TEXT; ALTER TABLE shared.users ADD COLUMN IF NOT EXISTS webauthn_challenge TEXT; ALTER TABLE shared.users ADD COLUMN IF NOT EXISTS has_seen_intro BOOLEAN DEFAULT FALSE; -- ============================================================================ -- 8. Stubbed email log table (for development — replaces real email sends) -- ============================================================================ CREATE TABLE IF NOT EXISTS shared.email_log ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), to_email VARCHAR(255) NOT NULL, subject VARCHAR(500) NOT NULL, body TEXT, template VARCHAR(100), metadata JSONB, sent_at TIMESTAMPTZ DEFAULT NOW() );