with ranked_active_codes as ( select id, row_number() over ( partition by tenant_id, phone, purpose order by created_at desc, id desc ) as row_no from public.sms_verification_codes where consumed_at is null and status in ('pending', 'sent') ) update public.sms_verification_codes code set status = 'expired', metadata = code.metadata || '{"expiredBy":"sms-active-reservation-migration"}'::jsonb from ranked_active_codes ranked where code.id = ranked.id and ranked.row_no > 1; create unique index if not exists idx_sms_codes_active_phone_reservation on public.sms_verification_codes (tenant_id, phone, purpose) where consumed_at is null and status in ('pending', 'sent'); create table if not exists app_private.sms_send_rate_limits ( tenant_id uuid not null references public.tenants(id) on delete cascade, dimension text not null check (dimension in ('tenant', 'phone', 'ip', 'device')), scope_hash text not null, bucket_start timestamptz not null, request_count integer not null default 0 check (request_count >= 0), updated_at timestamptz not null default now(), primary key (tenant_id, dimension, scope_hash, bucket_start) ); revoke all on table app_private.sms_send_rate_limits from public, anon, authenticated; alter table app_private.sms_send_rate_limits enable row level security; drop policy if exists platform_admin_sms_send_rate_limits on app_private.sms_send_rate_limits; create policy platform_admin_sms_send_rate_limits on app_private.sms_send_rate_limits for all using (app.is_platform_admin()) with check (app.is_platform_admin()); create index if not exists idx_sms_send_rate_limits_updated on app_private.sms_send_rate_limits (updated_at); comment on index public.idx_sms_codes_active_phone_reservation is 'Atomically permits one active SMS send reservation per tenant, phone and purpose.'; comment on table app_private.sms_send_rate_limits is 'Atomic hour/day SMS quota buckets. Dimension values are stored only as HMAC hashes.'; drop trigger if exists set_updated_at on app_private.sms_send_rate_limits; create trigger set_updated_at before update on app_private.sms_send_rate_limits for each row execute function app.touch_updated_at();