Dev.to Security 🔐 Cybersecurity 👁 0 📖 5 min read

AI Built Your Supabase App and It Works. Here's What Breaks Next.

A vibe-coded app that works is not the finish line — it's the starting line. The week-one demo passing tells you almost nothing about what happens in month one, when platform changes, key hygiene and permissive defaults

A vibe-coded app that works is not the finish line — it's the starting line. The week-one demo passing tells you almost nothing about what happens in month one, when platform changes, key hygiene and permissive defaults start collecting their dues. I audit these apps for a living; here are the five things that actually break them, each with the check that catches it before your users do.

All five come from real audits of real, shipped apps this month. No client data appears here — every example is from public repos, anonymized.

1. October 30: Supabase stops granting access to your new tables

The most time-sensitive one first. On October 30, 2026, Supabase stops auto-granting anon, authenticated and service_role on new tables in existing projects (changelog 45329). A table created after that date without an explicit GRANT returns 42501 permission denied through the Data API — even for service_role.

If your AI-built app creates tables after that date — late migrations, tenant-per-table schemes, an admin "add table" flow — it breaks with an error your users will find before you do.

The trap: the quick fix your search results (and your AI tool) will suggest is GRANT ALL ... TO public — which quietly re-opens exactly the exposure this change exists to prevent. The safe pattern is scoped grants per role, written into the migration itself:

-- in the migration, next to the table:
grant select, insert, update, delete on public.my_table to authenticated;
-- and explicitly NOT to anon, unless the table is truly public

The check (run today, takes 30 seconds):

select grantee, table_name, privilege_type
from information_schema.role_table_grants
where table_schema = 'public'
  and grantee in ('anon','authenticated','PUBLIC')
order by table_name;

Look for two things: tables you want accessible that currently rely on auto-grants (they'll need explicit grants before Oct 30), and any broad PUBLIC grants that shouldn't exist at all.

2. A real service-role key committed to the repo

This is the single most common critical I find in AI-built apps, and it's usually not dramatic — the key was needed for a script, a .env.local got committed once, nobody noticed.

One production app I audited this month had a committed env file with a real service_role JWT and a copy of it under a VITE_ variable — meaning the key that bypasses all RLS was one build step away from being baked into the public client bundle. Anyone visiting the site could have extracted it from the JavaScript.

The check: paste your committed service-role JWT into jwt.io. If the payload says "role": "service_role" and it still authenticates: rotate it first, in the Supabase dashboard — deleting the file does nothing, the value lives in git history forever.

The habit: the service-role key exists on servers, in server-side env, and nowhere else. Any variable name starting with VITE_, NEXT_PUBLIC_, REACT_APP_ is client code. If it's referencing a secret, the secret is already public.

3. A policy that says "Users can read" but means "anyone can read"

RLS enabled, policies present, dashboard green — and the table is fully readable with the anon key. This happens when a policy is written FOR SELECT USING (true) with no TO clause. In Postgres, no TO clause means the policy applies to public — which includes anon. The policy name says "Users can read all profiles"; the name doesn't execute anything.

A real marketplace I audited had exactly this on its profiles table — with every user's email in it. A login-page helper on another app (FOR SELECT TO anon USING (true) on the whole staff table, meant for one "does this email exist" check) exposed names, phone numbers and role assignments.

The check:

select tablename, policyname, cmd, qual
from pg_policies
where roles::text in ('{anon}','{public}') and qual = 'true';

Empty is the baseline. For login helpers, the fix isn't a policy at all — it's a SECURITY DEFINER function that returns EXISTS(...) while the table stays sealed.

4. No regression tests — so every "small fix" is a coin flip

Month one is when small fixes start landing: a client wants a field visible, someone grants a role too broadly, an AI tool "helpfully" relaxes a policy. Without a test that logs in as user B and asserts user A's rows don't come back, every one of those changes ships blind.

The minimum viable safety net is two queries and one fixture: seed a row per test user, switch to authenticated with a real JWT claim set (this detail matters — SET ROLE authenticated alone leaves auth.uid() NULL and every ownership policy filtering everything), and assert cross-user reads fail. My public repo (linked at the end) has the whole harness in a form you can run locally in about 60 seconds, no Docker and no Supabase project needed.

begin;
  set local role authenticated;
  select set_config('request.jwt.claims',
    json_build_object('sub','00000000-0000-0000-0000-000000000000','role','authenticated')::text, true);
  -- this is exactly what your app sees:
  select count(*) from public.notes;
rollback;

5. Nobody's watching the platform — and the platform does change

The Oct 30 change is not an anomaly; it's the norm. Stripe majors deprecate webhook fields, Next.js releases break getServerSideProps patterns, Postgres removes functions your queries lean on. An app nobody monitors accumulates these quietly until one Tuesday it's down and the founder finds out from a customer.

You don't need much — a monthly pass where someone who knows your stack reads the changelogs that touch it, checks your repo against each breaking change, and fixes the two that apply. That's it. (This is literally the retainer I run for solo founders — but even if it's you doing the pass yourself, put it on the calendar: month one is when "I'll deal with it later" becomes an outage.)

The 60-second version

  1. Run the grants query above (Oct 30 is three weeks away)
  2. Grep your repo for sb_secret_ and service-role JWTs; rotate anything real, immediately
  3. Run the policy query; kill every using (true) policy that's open to anon/public
  4. Steal the two-user isolation harness from my repo and add it to CI
  5. Put the monthly changelog pass on the calendar — or hand it to someone whose job it is

The runnable proof-of-concept for all of these is public: github.com/cekuu35/supabase-rls-leak-demo (the isolation harness, byte-for-byte identical tests on broken/fixed branches), and the free 30-second client-side scanner that checks items 2 and 3 on any public repo — in your browser, nothing stored — is at rls.cenkkurtoglu.com.

If reading this made you realize you'd rather have someone run the whole pass for you — that's the other thing I do, and my inbox is open.

All examples are from public-repo audits; no client data appears anywhere in this article. If you find something in someone else's app, tell them privately and precisely — that's how this whole practice started.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.