Gateway admin UI: 2FA/TOTP + backup codes, login MFA-race fix, and metrics observability diagnosis
the Gateway, the multi-client conversion gateway's Next.js admin console used with contractors, needed real access security and had a KPI panel that looked frozen. I deployed Supabase TOTP MFA (Google Authenticator) with single-use backup codes stored as SHA-256 hashes (migration 0007, RLS, service-role redemption that triggers an MFA reset rather than granting aal2), plus 30-minute inactivity auto-logout and 12h sessions. The login didn't reach the 2FA challenge on the first submit due to an AAL/session race; I specified reading the fresh assurance level after signInWithPassword and routing before the middleware reads a not-yet-propagated cookie. The 'frozen metrics' turned out not to be the backend, /v1/admin/metrics returns live figures matching a direct Firestore count with real aal1/aal2 JWTs, so I localized it to a stale dynamic SSR view / Netlify CDN quirk and shipped a real events-today endpoint, 30s auto-refresh, try/catch isolation and a composite index for exact per-client counts.
Proved the 'frozen' dashboard was a stale SSR/CDN view, the backend returned 2,810 live and matched Firestore exactly.
Role: Design + diagnosis; deploy via paired Claude Code
The Problem
the Gateway (the conversion gateway's admin console, used with contractors) needed access security (2FA, inactivity auto-logout) and its 'events today' metrics appeared frozen, undermining trust in the platform.
Supabase Auth has no native TOTP backup codes; the login flow didn't redirect to the MFA challenge on the first submit (required a refresh) due to an AAL/session race; and the KPI view looked stuck at 200, suspected to be a backend freeze.
The MFA redirect bug is a client-side timing race not reproducible headless; the 'frozen metrics' had to be distinguished from a real backend freeze (it wasn't) versus a stale SSR view / Netlify CDN cache.
Approach & Architecture
Deployed Supabase TOTP MFA (Google Authenticator) with single-use backup codes stored as SHA-256 hashes (migration 0007_mfa_backup_codes.sql, RLS + service-role redemption), 30-min inactivity auto-logout and 12h sessions. Diagnosed the login MFA race and specified computing the fresh AAL after signInWithPassword before navigating. Proved the metrics 'freeze' was not backend, /v1/admin/metrics returns live figures matching Firestore, and identified it as a stale dynamic SSR view / Netlify CDN quirk, fixed with a real events-today endpoint + 30s auto-refresh + try/catch isolation, plus a composite index for exact per-client counts.
Next.js (App Router) the Gateway on Netlify, Supabase Auth (gateway project) for MFA. Gateway exposes /v1/admin/metrics reading Firestore submissions live (countByStatus, no cache). the Gateway pages force-dynamic + no-store.
Key Decisions & Trade-offs
Hardest Part
Proving the metrics weren't actually frozen: verified /v1/admin/metrics returns live data matching a direct Firestore count with a real aal1/aal2 JWT, isolating the issue to a stale SSR view / Netlify CDN cache rather than the pipeline.
Technical Detail
mfa_backup_codes(id, user_id, code_hash, used_at, created_at) with RLS
Netlify (Next.js SSR force-dynamic/no-store), Supabase Auth, gateway /v1/admin/metrics over Firestore
Real login test (log out → log in → straight to 2FA); /v1/admin/metrics probed with real aal1 and aal2 JWT (200); metrics cross-checked against Firestore countByStatus
Code
create table if not exists mfa_backup_codes ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, code_hash text not null, used_at timestamptz, created_at timestamptz not null default now()); alter table mfa_backup_codes enable row level security; create policy own_backup_codes_select on mfa_backup_codes for select using (user_id = auth.uid());
await supabase.auth.signInWithPassword(...);
const { data } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel();
if (data.nextLevel === 'aal2' && data.currentLevel !== 'aal2') navigate('/mfa');Auth model and MFA-race fix designed in Claude Cowork and implemented/deployed by paired Claude Code; the metrics 'freeze' was diagnosed by having CC probe /v1/admin/metrics with real JWTs and compare to Firestore, ruling out a backend freeze and localizing it to SSR/CDN staleness.
Measured Results
the Gateway gained TOTP 2FA with hashed single-use backup codes, inactivity auto-logout and 12h sessions; the login now redirects straight to the 2FA challenge; and the 'frozen' KPIs were shown to be a stale SSR/CDN view, fixed with a real events-today endpoint, 30s auto-refresh and try/catch isolation plus exact per-client counts.
| Metric | Value | Before | Source |
|---|---|---|---|
| Live gateway metrics (global) | received 2810, delivered 2290, failed 517, in_transit 1 | n/a | GET /v1/admin/metrics vs Firestore countByStatus (CC) |
| received_today per client after the fix was previously capped at a 200 fallback | CL 1267, MX 391, AR 146 | n/a | Gateway per-client metrics endpoint |
Every figure above was recorded during the work itself. Where no number was measured, none is claimed.