-- 001_create_users.sql -- Auth tables for user management, sessions, and authentication codes. -- Compatible with both PostgreSQL (local dev) and CockroachDB (production). -- Core user identity. Email is the primary identifier for humans. CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, email_verified BOOL NOT NULL DEFAULT FALSE, name TEXT NOT NULL DEFAULT '', avatar_url TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'active', last_login_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Password credentials. Separate table because OAuth-only users have no password. CREATE TABLE IF NOT EXISTS user_passwords ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, password_hash TEXT NOT NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- OAuth provider connections (Google, GitHub, Apple, etc.). CREATE TABLE IF NOT EXISTS oauth_connections ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, provider TEXT NOT NULL, provider_user_id TEXT NOT NULL, provider_email TEXT NOT NULL DEFAULT '', access_token TEXT NOT NULL DEFAULT '', refresh_token TEXT NOT NULL DEFAULT '', token_expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (provider, provider_user_id) ); CREATE INDEX IF NOT EXISTS idx_oauth_connections_user_id ON oauth_connections (user_id); -- Verification codes for OTP login, magic links, password reset, and email verification. CREATE TABLE IF NOT EXISTS auth_codes ( id TEXT PRIMARY KEY, user_id TEXT REFERENCES users(id) ON DELETE CASCADE, email TEXT NOT NULL, code TEXT NOT NULL, purpose TEXT NOT NULL, expires_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ, ip_address TEXT NOT NULL DEFAULT '', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_auth_codes_email_purpose ON auth_codes (email, purpose, expires_at) WHERE used_at IS NULL; -- Sessions track where and when users are logged in. CREATE TABLE IF NOT EXISTS sessions ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, ip_address TEXT NOT NULL DEFAULT '', user_agent TEXT NOT NULL DEFAULT '', device_label TEXT NOT NULL DEFAULT '', last_active_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions (user_id) WHERE revoked_at IS NULL; -- User roles. Separate table so users can have multiple roles. CREATE TABLE IF NOT EXISTS user_roles ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, role TEXT NOT NULL, granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (user_id, role) );