MFA + step-up
TOTP for owners
Any user holding owner in any workspace is required to enroll TOTP before their next sensitive action. Enforcement is server-side via the app_private.user_has_mfa(user_id) helper, called at the top of the gated server actions.
Gated actions (planned set):
- Billing change — Stripe Checkout + Customer Portal
- Role change involving owner (promote / demote)
- Member removal
- Workspace deletion
- Impersonation start
The MFA enrolment UI is the remaining piece — until it lands, the helper still works but the gates are bypassed in dev with a warning. Don't deploy to production without enrolment wired.
Impersonation step-up
Impersonation specifically uses a 6-digit OTP delivered to the admin's email (or surfaced inline in dev mode). On verify, the server mints a 60-minute JWT that becomes the new effective session. See Audit log + impersonation for the JWT shape and the doubly-logged audit pattern.
AAL2
Supabase Auth's AAL2 (session-level MFA flag) is checked additionally for billing changes. AAL2 alone isn't sufficient — a malicious actor with magic-link access could otherwise sidestep MFA without ever enrolling. The server-side user_has_mfa check is the load-bearing gate; AAL2 is the freshness signal.