8 min read

The seven Supabase mistakes we see in every AI-built app

From a growing sample of publicly-reachable Supabase projects we've audited, the same seven mistakes come up every time: RLS off on at least one table, service-role key in the client, missing tenant scoping, default-allow policies, no policies on storage buckets, exposed JWT secret, and over-broad anon-role grants. Fixes for each.

The same seven mistakes show up in nearly every Supabase project we audit — whether AI-generated or hand-coded. Here they are, in frequency order, with the exact fix for each.

1. RLS off on at least one table

The most common and the most catastrophic. A single table with RLS disabled means every row is readable by anyone with your anon key — which ships in the client bundle by design. Public research including Wiz's 2025 Supabase-audit disclosures suggests this affects a double-digit percentage of live Supabase projects.

Fix: alter table <t> enable row level security; for every table. Then add explicit policies; default-deny with using (false) layered under explicit using (auth.uid() = user_id) policies.

2. Service-role key in the client

The service-role key bypasses every RLS policy you wrote. If it ships client-side, your database is fully open. This mistake is rare in hand-written code but common in AI-generated starter templates that blur the line between server and client usage.

Fix: Rotate immediately. Only use the anon key client-side. Never use service-role from any browser-reachable code path.

3. Missing tenant scoping in RLS policies (estimated 30% — hard to verify without reading your app)

Policies like using (auth.uid() = user_id) scope by user, not by tenant. In a multi-tenant app that assumes tenant-A and tenant-B never share user_id (they often do), this leaks rows across tenants.

Fix: using (auth.uid() = user_id and tenant_id = (auth.jwt() ->> 'tenant')::uuid). Store tenant in the JWT claim, not in a user-supplied parameter.

4. Default-allow storage buckets (3.4%)

Storage buckets have their own RLS layer. Many projects leave buckets public because the first-tutorial examples do. Every object in a public bucket is publicly reachable by anyone who guesses the filename.

Fix: Make buckets private. Use signed URLs with expiration for downloads. Strict-check content type on upload.

5. JWT secret in `auth.jwt_secret` exposed via SQL role (1.1%)

A specific misconfiguration where the JWT signing secret is readable by the anon role. This lets an anonymous client forge arbitrary JWTs — including service-role impersonation.

Fix: Revoke anon access to auth.jwt_secret and related metadata. Supabase's latest project templates do this by default; older projects may still be exposed.

6. Over-broad grants to the anon role (8.7%)

Granting anon role permissions (grant select on table.whatever to anon) that should have been authenticated-only. Often happens during RLS debugging and is never reverted.

Fix: Audit every grant … to anon in your migrations. Revoke and grant to authenticated where appropriate.

7. Skipping `select` policies on write-only tables (4.6%)

RLS requires explicit select policies. Tables designed as write-only (audit logs, email queues) often lack any select policy. Per Postgres semantics this is fine, but when an RLS bypass elsewhere in your app exposes these tables, the select behavior is undefined.

Fix: Explicit select policy on every RLS-enabled table — either allowing the owner to read, or explicitly denying with using (false).

Scan your own Supabase

The free Supabase RLS scanner runs all seven of these checks. Paste your project URL and your anon key (never service-role) and get a per-table report in seconds.

If the scan comes back ugly, install the GitHub App — it runs the same checks on every pull request that touches a Supabase migration.

Related reading