RowShield
Guides

Supabase security for solo founders: a one-person pass that holds

A solo founder ships the backend personally, which means the database configuration is reviewed by the same person who is rushing. Supabase makes that trade attractive: Postgres, authentication, storage and generated REST endpoints appear in an afternoon, and the anon key pasted into your frontend is public by design. The only mechanism bounding what that key can read is Row Level Security on every table.

Most one-person projects do not leak because the founder was careless. They leak because a migration created a table and forgot the policy pair, because a storage bucket was flipped to public for a demo and stayed that way, or because the service role key ended up somewhere a browser can fetch. None of these produce an error; every affected request returns a normal-looking success, which is why discovery tends to arrive late and from outside.

RowShield does not detect this yet. This guide gives you the catalog queries to check it yourself. The nine rules that do ship are listed on the rules index.

The two failures worth fixing first

The first failure is a table in the public schema with RLS disabled. Your appointments table qualifies the moment a migration creates it without its policies — names, phone numbers and booking times served to anyone presenting the project URL and anon key. Nothing about the request looks wrong afterwards: status 200, tidy JSON, no error anywhere in your logs.

The second failure looks safer and is not. A policy whose condition reduces to constant truth grants every row to everyone, and a table with RLS enabled but zero policies denies everything, breaking your app quietly instead of leaking it loudly. Both are configuration states rather than code bugs, so they persist silently until somebody reads the catalogue and checks.

The thirty-minute pass

Open the SQL editor and ask the catalogue directly. One query lists every user-reachable table and whether RLS is switched on; a second covers storage buckets flagged public. Run them before changing anything and note what you find — the list is your baseline, and the point of a solo pass is knowing your actual position rather than a remembered one:

SELECT c.relname AS table_name,
       c.relrowsecurity AS rls_enabled
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = 'public'
  AND c.relkind IN ('r', 'p')
ORDER BY c.relname;

Fixing the appointments table properly

Next, search your repository for the service role key: it belongs in server-side environments only, and if it appears anywhere a browser can reach, rotate it immediately and treat the old value as burned. Then fix the appointments table with three statements — enable, force so the owner cannot bypass, and policies scoped to user_id, with WITH CHECK guarding inserts:

ALTER TABLE public.appointments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.appointments FORCE ROW LEVEL SECURITY;

CREATE POLICY "clients_read_own_appointments"
  ON public.appointments
  FOR SELECT
  TO authenticated
  USING (user_id = (SELECT auth.uid()));

CREATE POLICY "clients_write_own_appointments"
  ON public.appointments
  FOR INSERT
  TO authenticated
  WITH CHECK (user_id = (SELECT auth.uid()));

Applying it without breaking your weekend

Apply enablement and policies in one deploy, then exercise the app end to end before celebrating. Expect one quiet breakage somewhere: an admin screen that relied on blanket access, or a stats endpoint reading through the anon role. Each breakage is information about an access path you had forgotten you built, and each deserves its own scoped policy rather than a rollback.

Finally, walk Storage deliberately and confirm each bucket’s publicity. Public-by-accident is the classic demo leftover, it needs no exploit to read, and it bypasses every careful table policy at a stroke. Decide once, in writing, which files genuinely belong on the open internet.

Keeping it fixed as a one-person team

The uncomfortable arithmetic of solo work is that fixes decay: next month’s feature ships another table and the migration omits the pair of lines again, because nothing in a busy week remembers March. Monitoring therefore has to be mechanical rather than mnemonic — a scheduled comparison of current configuration against last known state, reporting changes.

RowShield does not automate this checklist — the judgment calls stay yours — but the watching is exactly what it does. A scan records every table’s state, later scans diff against it, alerts fire on transitions only, and scans run daily on Free, hourly on Indie and every fifteen minutes on Team. RowShield reads pg_catalog and bucket metadata, never rows, and is an independent product unaffiliated with Supabase.

Frequently asked

Should I worry that the anon key is visible in my frontend?
No. The anon key is designed to be public, and treating it as a secret leads nowhere useful. The question that matters is whether every table it can reach has Row Level Security with policies bounding what an unsigned-in caller receives. Fix the policies and keep shipping with the key in the bundle.
How long should a first pass take?
About an evening for a typical side-project schema: half an hour to run the catalogue checks and hunt leaked keys, an hour to write policies for the handful of tables that matter, the rest exercising the app. Anything larger suggests the schema has outgrown what one person holds in their head.
Do I need to do this before launch?
Before, ideally. Public exposure does not wait for traffic — a table without RLS leaks rows to the first curious visitor, whether that visitor arrives on day one or day four hundred. Locking down early costs one deploy; locking down after an incident costs explanations as well.

Check your project in about ten seconds

Paste a URL. No signup, no writes, nothing stored.

Run the free audit
supabase security solo foundersecure supabase side projectsupabase rls checklist indie developersupabase anon key exposure