Supabase security preflight: 11 checks before your app goes live

Before you launch a Supabase app built with Lovable, Bolt, Cursor, or Replit, verify eleven boundaries the builder never saw: row level security on every table, WITH CHECK on every write policy, no service role key in the browser, production redirect URLs, storage policies, and a real two-user test. Every one of these is a failure we keep getting called to fix after launch.

This is the checklist we run first on every rescue. It is free because most of it takes minutes to check yourself, and because the apps that skip it become our emergency calls later, at a worse moment and a worse price than prevention.

The checklist

RLS is enabled on every table, not just the obvious ones

Run: select tablename, rowsecurity from pg_tables where schemaname = 'public'. Any table with rowsecurity false is fully readable and writable with the anon key. Junction tables, logs, and lookup tables get forgotten most often.

Every INSERT and UPDATE policy has a WITH CHECK clause

A policy with USING but no WITH CHECK controls what users can read, not what they can write. This is how apps pass every manual test and still let one user write rows as another.

No policy quietly evaluates to true for everyone

RLS being ON is not the same as RLS doing something. A policy like using (true) on a private table, or a role check that matches every logged-in user, protects nothing while making the dashboard look green.

The service role key exists only on the server

Search your client bundle for service_role. If it ships to the browser in any form, every table is open regardless of policies. Only the anon or publishable key belongs client-side.

Sign-up does not leak which emails already have accounts

Try signing up with an existing email and a wrong password flow. If the error text differs from a fresh email, you have user enumeration. Normalize the responses and rate-limit the endpoints.

Auth redirect URLs list the real production domain

The number one works-in-preview, breaks-live cause. The Site URL and every redirect URL must include the deployed domain, exactly, protocol included.

Magic links survive corporate inboxes, or you use OTP codes

Email security scanners open links before the user does and burn the one-time token, which surfaces as otp_expired on a fresh link. The typed 6-digit code flow with verifyOtp is immune.

Storage buckets have their own policies

Bucket policies are separate from table RLS. A public bucket with guessable paths is an open directory of your users' uploads, no matter how tight the database is.

Production env vars are present AND non-empty

An empty environment variable fails silently: the page renders, the button does nothing, and no error reaches you. Check the deployed values, not the .env file on your laptop. We have watched an empty var kill a checkout for weeks.

Two-user test: user A genuinely cannot touch user B's rows

Create two real accounts. As A, try to read and write B's data through the app AND through direct REST calls with A's token. The app hiding a button is not enforcement; the database refusing is.

The post-insert read is covered by a SELECT policy

Supabase client inserts read the row back unless you ask for minimal returning. A missing SELECT policy makes valid inserts fail with 42501 and sends you debugging the wrong policy.

What to do with a failure

If one of these checks fails and you know how to fix it, fix it before launch; every item above is cheaper to fix today than after real users and real data arrive. If a check fails and you are not sure what the fix means for your app, paste the failing check and your exact error into the free diagnosis form and we will tell you, in plain language, what is wrong and what the fix involves. If you want it fixed for you, our founding fix is a flat $90 and you pay after it works on your live app.