afterbuild/ops
Resource · Security

Supabase RLS for non-technical founders.

A plain-English guide to the database rule that stops user A from reading user B's data — and the exact checklist to verify yours is right.

By Hyder ShahFounder · Afterbuild LabsLast updated 2026-04-15

TL;DR (55 words)

Row Level Security is a database rule that forces “user A sees only their rows, user B sees only theirs.” Lovable, Bolt, and most AI-built apps ship it disabled or permissive, because the preview uses a key that ignores it. Enable RLS on every table, write an `auth.uid() = user_id` policy per path, and test with a second user account.

By Hyder Shah · Published 2026-04-15 · Updated 2026-04-15

What is RLS, in one paragraph?

A database table is a spreadsheet. Every row belongs to someone — usually a user. Without Row-Level Security, the database treats every authenticated user the same: if you're signed in, you can query any row in the table. RLS changes that. It tells the database “only return the rows where this user owns the row.” The rule is enforced on every query, on the database side, with no way to bypass from the client. If the rule isn't there, any signed-in user can read any row.

Why this matters for your app

Supabase is the default database for Lovable, Bolt, v0, and many other AI builders. Every one of them ships apps that, by default, do not enable RLS. The Register's February 2026 reporting catalogued 170 Lovable apps leaking data for 18,000+ users through this exact gap. The pattern is simple: if a pilot user signs in and opens the browser's network tab, they can see the Supabase API endpoints the app calls. They can call those endpoints themselves, with their own session token, for any table. Without RLS, they get back every row.

Why Lovable / Bolt / v0 ships RLS broken

Two reasons, both structural:

  1. The preview uses a superuser key. AI builders give the app a service-role Supabase key in preview, which bypasses RLS entirely. The preview works with or without policies — so the builder has no incentive to write them.
  2. Generated policies are usually permissive.When AI builders do generate a policy, it's frequently `USING (true)` — which allows every row to every user — because that's the policy that makes every prompt work. This is the same as no policy, but harder to spot.

The result: most AI-built Supabase apps have a disclosure vulnerability that only becomes visible after launch. Veracode's 2025 study puts the broader rate of AI-generated code with security vulnerabilities at 48% — with disabled or permissive RLS being the single most common pattern in Supabase apps.

The plain-English checklist

Work through these in order. You don't need to write SQL — Supabase's dashboard has UI for every step.

  1. Open Supabase → Authentication → Policies.You'll see every table in your database and whether RLS is enabled.
  2. For every table that stores user data, enable RLS.Click “Enable RLS” on each. Your app will now block all reads and writes to that table until you write a policy — this is fine; we're about to.
  3. For each table, add a “user owns row” policy.The pattern: “Policy applies when `auth.uid() = user_id`”. This says: a user can only see rows where the `user_id` column matches their authenticated user ID.
  4. Add separate policies for `SELECT`, `INSERT`, `UPDATE`, `DELETE`. Each operation needs its own policy. A common shortcut: `FOR ALL USING (auth.uid() = user_id)`.
  5. Audit any policy with `USING (true)` and remove or rewrite it. These are the permissive policies Lovable and similar tools generate; they are functionally equivalent to no policy.
  6. For multi-tenant apps, join through a tenant ID column. If your rows belong to an organisation rather than a user, policies should check membership via a join on the memberships table.
  7. Test with a second user account.Sign in as user A, create data, sign out, sign in as user B. Open user A's URLs. You should see 403 or empty results, never user A's data.
  8. Add an automated RLS test. A pgTAP test suite in CI fails the build if a policy is missing. Setting this up once prevents regression forever; we include it in every production-readiness pass.

Common RLS policy patterns

Use casePolicy pattern
User owns rowauth.uid() = user_id
User belongs to tenanttenant_id IN (SELECT tenant_id FROM memberships WHERE user_id = auth.uid())
Public read, owner writeSELECT: true · UPDATE/DELETE: auth.uid() = user_id
Admin overrideauth.uid() IN (SELECT user_id FROM admins)
Service-role only(no policy — use service key on server)

What RLS does not protect

The fastest way to verify your app

  1. Create two test accounts. Call them red and blue.
  2. Sign in as red. Create a piece of data. Note its ID.
  3. Open your browser's network tab. Copy the Supabase URL pattern the app uses.
  4. Sign out. Sign in as blue.
  5. Manually call the Supabase endpoint for red's data ID using blue's session.
  6. If blue sees red's data — RLS is broken. Book a diagnostic.

When to get help

If you're non-technical and the checklist above is past the edge of what you can do alone, book our Security Audit. It's a fixed-fee RLS and security pass — every table audited, every policy written, a pgTAP test suite installed in CI, and a written report. For broader rescue work that includes RLS plus auth plus Stripe plus deploy, the Deploy-to-Production pass is the right scope.

Related reading

FAQ
What is Row Level Security in plain English?
RLS is a rule that lives on each database table and says who can read or write each row. Without RLS, any authenticated user can read every row in the table. With RLS, the database itself enforces 'user A sees user A's rows; user B sees user B's rows' on every query, with no way to bypass from the client.
Why do Lovable and Bolt apps ship without RLS?
Their preview environments connect to Supabase using the service-role key, which bypasses RLS. The app works in preview even if no policies exist. On deploy, the app uses the anon key — which respects RLS — but if no policies exist, users can read everything. The Register documented 170 Lovable apps exposed this way in February 2026.
How do I check if my app has RLS enabled?
In the Supabase dashboard, open Authentication → Policies. If every table shows 'RLS disabled' or 'No policies', your app is vulnerable. Every table that stores user data needs RLS enabled and at least one policy per read/write path. Our checklist below walks through the exact steps.
Can I just enable RLS and be done?
No. Enabling RLS without policies blocks all reads and writes — your app will break. You need to enable RLS and then write policies that allow the correct access. The most common pattern is 'auth.uid() = user_id', which lets each user see only their own rows.
Is RLS enough on its own?
For most apps, yes — if it's written correctly. But RLS is a database-level rule; anything that bypasses the database (server-side code using the service-role key, admin dashboards, cron jobs) needs separate access control. RLS plus disciplined use of the anon key on the client is the 95% pattern.
What's the single most common RLS mistake?
A permissive 'true' policy that allows everything. Lovable generates these to make the preview work, and they ship to production unchanged. If any of your policies has 'USING (true)' without a real condition, it's the same as no policy.
How do I test that RLS is working?
Two ways: (1) pgTAP test suite in CI that asserts cross-tenant reads fail; (2) a manual test — sign in as user A, copy their session token, sign in as user B, try to query user A's data. If it succeeds, RLS is broken.
Does Afterbuild Labs fix RLS?
Yes — it's part of every production-readiness pass and every rescue. A standalone RLS audit and policy write-up is included in our Security Audit service (fixed fee). See the service page or book a free diagnostic.
Next step

Not sure if your RLS is correct?

Book a free 30-minute diagnostic. We'll audit your Supabase policies and tell you what's missing.

Book free diagnostic →