Confirmed — three domain occurrences, the section header, and brian@loser.com all need scrubbing. Proceeding now.
TLDR: An app-layer auth gate doesn't protect you when the auto-generated REST API accepts any valid Google JWT. Lock the OAuth door first, then gate the UI.
The Setup
An ecommerce client runs five apps off a single shared Supabase project — a KOL CRM, a webinar planner, a DAM, a Shopify sync, and more.
Every app had an access gate: is_allowed_user(), checked at the app layer, keyed to @example.com and @examplelabs.com domains.
That felt solid.
It wasn't.
What My Developer Found
A developer I work with noticed the OAuth was wide open.
"Any Google account can authenticate to Supabase and read/write tables straight through PostgREST — the app-layer gate never sees it."
PostgREST (Supabase's auto-generated REST API, the engine behind every supabase.from(...).select() call) accepts any valid JWT. My app-layer check only ran inside the app. Someone with a raw authenticated JWT and a REST client could walk right past it.
The stakes weren't theoretical. That shared project held 56,193 Shopify orders, refund records, and patient-adjacent health-commerce data.
What I'd Done Wrong — Honestly
Here's the embarrassing part: I had deliberately left the Google OAuth domain restriction off.
My reasoning: the project's single OAuth client also served the webinar app, which occasionally onboards external collaborators who aren't @example.com or @examplelabs.com. I didn't want to lock them out.
So I built the app-layer gate and called it done.
When we audited auth.users before locking, we found exactly zero external collaborators had ever authenticated.
I'd left the door open for guests who never came.
The Fix
Supabase's Before-User-Created Hook (Dashboard → Authentication → Hooks) lets you run a Postgres function that fires before any new auth signup completes.
I wrote public.hook_restrict_signup_by_domain(event jsonb) — it reads event->'user'->>'email', extracts the domain with split_part(email,'@',2), and either returns {} to allow or {"error":{"http_code":403,"message":"..."}} to reject.
Allowlist: example.com and examplelabs.com.
Two critical grants that are easy to miss:
GRANT EXECUTE ON FUNCTION public.hook_restrict_signup_by_domain TO supabase_auth_admin;
REVOKE EXECUTE ON FUNCTION public.hook_restrict_signup_by_domain FROM authenticated, anon, public;
Skip those and it either silently fails or becomes callable by the wrong roles.
The Gotcha That Catches Everyone
The hook only blocks new signups.
Existing auth.users rows are untouched. The audit found one out-of-domain account: dev@external.com — a trusted dev/test account that, ironically, proved the hole existed. We kept it intentionally.
Run this to find yours before you lock:
SELECT split_part(email,'@',2) AS domain, count(*)
FROM auth.users GROUP BY 1;
Deal with any stragglers before you celebrate.
Why This Matters to Me
I assumed the threat was "unauthorized users inside the app."
The real threat was "any Google account, skipping the app entirely."
App-layer gates guard the front door. But if your backend API accepts any valid JWT from your open OAuth, there's a side door you haven't thought about.
Lock the OAuth first. Then gate the app.
P.S. If you're on a shared Supabase project serving multiple apps, your allowlist has to include every legit domain across all of them — or real users lose access the moment they try to re-auth.