TLDR: We had 13 Supabase RLS policies set to using(true). That meant any Google account in the world could read our Shopify orders through PostgREST — completely bypassing our app-layer auth. Here's what we found, what we got wrong first, and the fix that actually worked.

The Setup

We're building our internal business operations suite — a stack of internal tools for an ecommerce business — on a shared Supabase project (Supabase, a Postgres-based backend-as-a-service).

The app required login. We had auth middleware. We felt fine.

We were wrong.

What a Colleague Found

A developer I work with flagged something quietly alarming: our RLS (Row-Level Security, Postgres's built-in table-access filter) policies were written as to authenticated using(true).

That sounds fine. "Authenticated" users only, right?

Here's the problem nobody talks about: Supabase's PostgREST layer treats any Google OAuth token as authenticated. Not your Google account. Any Google account.

So "authenticated" didn't mean "an employee." It meant "literally anyone who has a Gmail address."

Our app checked auth. Our database… did not.

The Exposure

We ran the query:

select count(*) from public.shopify_orders;

...impersonating a random attacker JWT. The number that came back?

56,193 rows.

Full order history. Refund records. Recharge subscription data. All of it readable by any Google login, straight through PostgREST — zero app code involved.

What I Tried First (and Why It Was Wrong)

A few days earlier we'd fixed the same problem on a different app — our webinar project management app — using an email allowlist. We wrote a webinar_is_allowed() function that checked JWTs against a webinar_email_allowlist table, and gated all 29 policies on it.

It worked great over there. So naturally I started down the same path for our internal business operations suite.

Then I caught myself.

The webinar project management app's allowlist is a tight list of webinar collaborators. Our internal business operations suite staff — supply chain, KOL CRM, operations — aren't on it. Reusing webinar_is_allowed() would have locked out our own team the moment they hit a route in our internal business operations suite.

Wrong tool for the job.

The Fix That Worked

The right answer was already in the codebase. Our KOL CRM module's src/lib/auth.ts was already checking email domains against our two company domains. So we mirrored that logic in Postgres.

Migration 019_rls_close_open_policies.sql replaced every using(true) with public.is_allowed_user() — a security-definer function that checks the JWT's email claim against those two domains.

13 policies. One migration. Closed.

Verify the Fix — Don't Trust the Toast

This matters: the Supabase SQL editor shows a "Success" banner even when your policy does nothing useful. We've been burned by this.

The real verify is an impersonation probe — you simulate exactly what PostgREST does with a non-allowed JWT:

begin;
select set_config('request.jwt.claims','{"email":"attacker@gmail.com","role":"authenticated"}', true);
set local role authenticated;
select count(*) from public.shopify_orders;  -- must return 0
rollback;

Ours returned 0. Then we tried an insert into a gated table — got back error 42501, RLS violation.

That's when you know it's real.

Why This Matters to Me

The lesson is embarrassingly simple once you see it: app-layer auth and database-layer auth are two completely separate systems. Your middleware gate protects your routes. It does not protect your data if someone queries PostgREST directly.

Every Supabase table with RLS enabled but no real policy is a using(true) waiting to happen. And using(true) with to authenticated isn't "logged-in users only" — it's "every Google account on earth."

Audit your pg_policies. Run the impersonation probe. Don't trust the toast.

P.S. If you're on a shared Supabase project with multiple apps, be careful about sharing allowlist functions across them. webinar_is_allowed() was right for the webinar project management app. It would have silently locked out our ops team if I'd copy-pasted it here. Domain gates are safer defaults when the user population is "staff."