PRACTICE APP, NOT A CUSTOMER. This is our own demo project, built for testing. All data is fake.

Case study Practice app

Three planted holes in our practice Supabase app, and what each check shows

Practice app, not a customer. All data is fake.

We built a demo Supabase project, patchandship-demo, with fake data, planted three common holes, and compared what each check reports. Each one answers a different question, so this isn't a ranking.

This is a demo on our own practice app, not a penetration test or a compliance audit. It shows what three checks report on one project.

The three holes

  1. RLS turned off. The table public.demo_customers has row level security disabled. The anon and authenticated roles have select, insert, update and delete grants.
  2. A read policy that allows everyone. On public.demo_orders RLS is on, but the policy "demo: anyone can read orders" is for select to anon, authenticated using (true). It looks protected, but anyone can read every row.
  3. A public file in a public bucket. The public bucket demo-public holds a fake invoice, FAKE-invoice-0001.txt. A signed-out request to its public URL returned HTTP 200 (245 bytes). Listing the bucket as anon returned an empty list, so the file is reachable by anyone who has, or guesses, its link.

Supabase Security Advisor

The Security Advisor is free and built in. It lints your schema and configuration. On the demo it reported 1 error, 0 warnings and 0 suggestions:

Supabase Security Advisor on the PatchAndShip demo project, showing 1 error: "RLS Disabled in Public" on public.demo_customers, with 0 warnings and 0 suggestions.
Practice app, not a customer. All data is fake. Screenshot taken Oct 15, 2026.

Supabase's Security Advisor flagged the table with RLS turned off. It didn't flag a table whose RLS policy lets anyone read every row, because its always-true-policy check skips read policies by design. It also doesn't flag public buckets that serve files by link.

Both are design choices. A using (true) read policy is often a real public catalog, and a public bucket is meant to serve files by link. The Advisor can't know which you meant.

Our open-source RLS checker (not yet released)

check-my-supabase is an open-source script we're preparing. It is not published yet. It checks what a signed-out visitor can actually read, with read-only HEAD requests as the anon role. It never downloads rows.

We ran version 0.1.0 against the demo on Oct 15, 2026:

  READABLE   demo_customers  returns rows (4)
  READABLE   demo_orders     returns rows (4)

2 tables return rows to anyone with your public key.

Practice app, not a customer. All data is fake.

Its limits: it shows that a table is readable, not why, so it can't tell hole 1 from hole 2. It doesn't test writes or Storage, or know whether a table should be public.

Writes matter. On Oct 15, 2026, we sent a signed-out insert to demo_customers with only the public key. It worked, and we deleted the fake test row right away. The same settings leave updates and deletes open too, though we tested only the insert. The same insert into demo_orders was refused with error 42501.

Our free website check

The free check reads your public homepage, scripts and headers. It never queries a database, so it can't see RLS problems like holes 1 and 2. The demo has no website, so we didn't run it. From its current code:

  • Holes 1 and 2: not detected. It calls your public Supabase key normal and points you to the Advisor.
  • Hole 3: noted only if your homepage or its scripts build a public link to demo-public. It never opens or lists the bucket.
  • It covers what the others don't: secret keys in your public JavaScript (details shown only after you verify you own the site) and missing security headers.

Before and after: one fix on our practice app (Oct 31, 2026)

Practice app, not a customer. Fake names only.

PatchAndShip DEMO APP. One problem, then fixed. Before: Anyone could read this app's user list, shown as a table of three fake rows, Demo Person One, Two and Three, with masked emails, labeled fake demo data. After: Now that list is private, with a padlock and the line Signed out visitors see nothing. Small print: Our own demo app with fake data. Not a customer. Demo test: same request, before 200 with 3 rows, after 401 permission denied. The public key can no longer read the table.
Demo before and after on our own demo app (fake data).

On Oct 31 we added a new table to the same practice project, public.demo_signups, with three fake rows. RLS was off and signed-out visitors had read access. With only the public key, anyone could read every name and email: the request returned HTTP 200 with 3 rows.

One fix: we turned on row level security, took away the signed-out access, and added a rule so each signed-in user only sees their own rows. The same request now returns HTTP 401, "permission denied" (error 42501). A signed-out visitor gets nothing.

What the paid fix adds

Security Fix ($149) covers RLS fixes on all tables (up to 25 user-defined tables), storage bucket policies, key scan and rotation guidance, and a short report. Fix & Deploy ($249) adds production deploy, env vars, auth redirects, custom domain, SSL and a smoke test of key flows. For an exposed secret key, 'fixed' means we've removed it from your code in the change we deliver. You redeploy your app, set the key on your host and rotate it. We give you exact instructions. This practice app hasn't had a paid fix, and we have no customer results to show.

Takeaway

A clean Security Advisor means your schema passed its lints, not that signed-out visitors can't read anything. Run the Advisor, then check what's actually readable.

Questions: patchandship@gmail.com

PatchAndShip Team