-- 0002_mfa_recovery.sql -- Additive, idempotent migration for mandatory-MFA recovery codes. -- Safe to run multiple times. -- 1. Recovery codes ------------------------------------------------------- -- One row per single-use recovery code. We only ever store a SHA-256 hash of -- the code; the plaintext is shown to the user exactly once at generation. create table if not exists public.mfa_recovery_codes ( id bigint generated always as identity primary key, user_id uuid not null, code_hash text not null, used_at timestamptz, created_at timestamptz not null default now() ); create index if not exists mfa_recovery_codes_user_id_idx on public.mfa_recovery_codes (user_id); -- Fast lookup of an unused code by (user, hash) during redemption. create unique index if not exists mfa_recovery_codes_user_hash_uidx on public.mfa_recovery_codes (user_id, code_hash); -- 2. Lock down with RLS, NO policies --------------------------------------- -- RLS enabled with NO policies so anon/authenticated roles cannot read or -- write recovery codes at all. The application generates and redeems codes -- exclusively through the service-role client, which bypasses RLS. alter table public.mfa_recovery_codes enable row level security; comment on table public.mfa_recovery_codes is 'Single-use MFA recovery codes (SHA-256 hashed). RLS enabled with no policies: service-role only.';