The full plan to turn today's client-side login gate into a real security boundary — per-user identity, Row-Level Security as the data boundary, and TOTP/AAL2 multi-factor at sign-in. Sequenced so no one gets locked out, every step reversible.
Ground truth from the live database and Auth config, 7/6/26.
Most tables already have RLS enabled with policies — but the app talks to Postgres with the anon key (pure-auth.js sends Bearer ANON on every request; pure-mcp runs verify_jwt=false), and the existing policies are permissive enough to let anon read/write so the prototype works. RLS is a gate with the door propped open.
So this is less "build from zero" and more flip the app from anon to real per-user identity, then tighten the policies. MFA is similar: Supabase Auth has native TOTP/AAL2 and the pure-mfa.html design preview exists — it needs enabling and wiring, not inventing.
service_role — so those tables already return empty to the app and only the MCP/cron service-role path reads them. The app tolerates this today. Adding policies is additive; the danger is enabling RLS on the 39 still-off tables without a SELECT policy in place first (instant blank).Sign-in works verify staff can actually authenticate THEN RLS read-only THEN RLS writes THEN MFA enforcement. Never enable enforcement before identities can log in, or staff get blanked mid-session. Every step lands behind a flag and is reversible, and a break-glass service-role admin path stays open throughout.
Prerequisite for everything — nothing below means anything until the app sends a real user token.
PureAuth.authedFetch sends Bearer ANON. Send the signed-in user's access_token (already in pure.auth.session) for PostgREST/RPC calls, anon only for genuinely public reads. This one change makes RLS start meaning something.auth.users row + role in app_metadata (server-controlled), not user_metadata (user-editable). Migrate the role source so RLS can trust it.pure_orgs, pure_roles, pure_identity_hats with FKs — the model in the Org Hierarchy doc). Policies reference these to answer "can this user see this row?" without hard-coding role strings.Tighten the propped-open door. Reads before writes, tier by tier.
using (true) / anon-allowing). Sort every table into: public-read (brand, icons, FAQ), authenticated-read (most app data), owner/org-scoped (deals, CDA, money, PII), admin-only (audit_log, settings, deploy registry).current_hats(), has_role(), in_org() that read the JWT (auth.jwt(), app_metadata.role) against pure_identity_hats. Every policy calls these instead of re-deriving.t-rec-rls-readonly ticket), table-tier by tier. Worst case is an over-restrictive read — instantly revertible, can't destroy data.SECURITY DEFINER RPCs (the boneyard / security-posture pattern). Anon loses all write grants.pure-mcp to verify_jwt=true for authenticated endpoints; keep chat.send / public reads on a separate anon-allowed path. Deploy via ComposioGitHub.Step-up authentication. The infrastructure exists — enable and wire it.
pure-mfa.html's enroll / challenge / verify to PureAuth (mfa.enroll, mfa.challenge, mfa.verify). The design preview already exists.mfa_required flag. Admins and any opted-in org require AAL2.aal === 'aal2' for sensitive routes; RLS policies check auth.jwt()->>'aal' server-side.authedFetch/PureLive.rpc() and graceful 401/403 handling — bounce to sign-in, or the "not authorized for this role" panel (the role-guard already built).security-posture.html is the live scoreboard — rls flips partialhave and mfaenforced as each lands. pure-protection-audit.html tracks page coverage.Each row is its own reversible checkpoint — one per turn.
| # | Checkpoint | Guard |
|---|---|---|
| 1 | Client sends real JWT (A1) | Test with admin first |
| 2 | Org/role catalog live (A3) | Additive |
| 3 | Policy helpers + READ-only RLS, tier by tier (B5–B6) | Watch over-restriction |
| 4 | WRITE policies + RPC-only privileged writes (B7) | Break-glass admin |
| 5 | verify_jwt=true on pure-mcp (B8) | Split public path |
| 6 | TOTP enroll live, opt-in (C9) | Non-enforcing |
| 7 | AAL2 enforced — admins, then per-org (C10–C11) | Recovery codes ready |
property.html?), public chat — keep a deliberate anon-read policy set; don't blanket-deny.authedFetch caller so nothing fetches with a stale anon token after the flip.The trap: 36 users, 3 factors. A global “require AAL2” flip strands 33 people at a dead screen. The fix is to never flip — enforcement becomes a self-enrollment grace gate with a pre-flight guard that makes lock-out structurally impossible.
pure-mfa.html enrollment, which they complete in-session and continue. AAL1 is enough to reach the enrollment screen; AAL2 is required for everything else. A factorless user can always self-serve out of the trap — there is no state where you can log in but cannot enroll.mfa_required for an org starts a countdown (default 14 days). During grace, AAL1 users keep full access behind a persistent “Set up MFA before <date>” banner. After expiry, enrollment is required before continuing — still self-serve in the same session, still never a lockout.SECURITY DEFINER RPC mfa_enforce(org) refuses to arm enforcement unless: (a) ≥1 admin in that org has a verified factor, (b) the enrollment route is reachable, (c) recovery-code issuance is on. You literally cannot enable MFA for an org that would lock itself out.pure_admin_allowlist MFA-exempt entry — a permanent, monitored break-glass account that AAL2 enforcement never applies to.admin_mfa_reset(user) service-role RPC clears a stuck user’s factors (audited to audit_log) so they re-enroll clean.pure_mfa_recovery; one-time codes get a user past a lost device without admin help.mfa.enforcement flag the gate reads live — setting it off reverts everyone to AAL1-optional instantly, no deploy.Exactly what changes behavior at each step, and how to undo it. Login itself never breaks — you can always authenticate; the risk is “logged in but blank,” which is always reversible.
| Change | What could break | Rollback |
|---|---|---|
| Client sends real JWT instead of anon (A1) | Any page reading a table whose policy only allowed anon stops returning rows once the token changes. | Revert authedFetch to anon; one file, one deploy. |
Tighten pure_identity_hats (anon authenticated) | Hats dropdown + Act-As go blank if read before JWT attaches or org-scope too strict. Highest-touch table — test hardest. | Restore the permissive policy (kept as a named migration); instant. |
Lock pure_properties (public authenticated) | Public property search / listing microsites stop working for logged-out visitors. | Keep a deliberate public-read policy set; decide per-table, don’t blanket-deny. |
| Enable RLS on the 39 still-off tables | Instant blank if no SELECT policy exists first. | Add policy first, then enable; or disable RLS on that table. |
verify_jwt=true on pure-mcp (B8) | Public paths (chat.send, board reads) 401 if not split onto an anon path. | Split public endpoints first; redeploy prior function version via Composio. |
| AAL2 enforcement (C10–C11) | Factorless users routed to enrollment (by design, not a lockout — see §4). | mfa.enforcement kill switch off; or admin_mfa_reset. |
Two doors stay outside the app’s own auth at all times: the Supabase dashboard (owner login) and the service_role key (bypasses all RLS, powers MCP + crons). As long as both are retained, any soft-lock is a few-minutes fix, never a dead end. Do not route either of those behind the app gate.
| Owner | Scope |
|---|---|
| Code | All SQL (policies, helpers, RPCs), verify_jwt, Auth TOTP config, app_metadata role migration. |
| Mike | Confirm any paid add-ons (none strictly required for RLS/MFA), approve the enforcement cutover. |
| Design | JWT-aware authedFetch, 401/403 UX, MFA screens wired, org/role catalog tables, security-posture + protection-audit scoreboards, the rollout checkpoints. |
This document is the plan of record for the auth window. The natural next step is a board epic (parent ticket + the 7 rollout checkpoints as children) and a contract-first SECURITY-AUTH-WINDOW-SPEC.md that Code builds the SQL against — keeping the reversible, one-checkpoint-per-turn cadence.
PURE MLS Technologies · Auth Window Implementation Plan · prepared by Lane A (Shattique) · 7/6/26