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:
- 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.
- 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.
- Open Supabase → Authentication → Policies.You'll see every table in your database and whether RLS is enabled.
- 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.
- 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.
- Add separate policies for `SELECT`, `INSERT`, `UPDATE`, `DELETE`. Each operation needs its own policy. A common shortcut: `FOR ALL USING (auth.uid() = user_id)`.
- 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.
- 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.
- 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.
- 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 case | Policy pattern |
|---|---|
| User owns row | auth.uid() = user_id |
| User belongs to tenant | tenant_id IN (SELECT tenant_id FROM memberships WHERE user_id = auth.uid()) |
| Public read, owner write | SELECT: true · UPDATE/DELETE: auth.uid() = user_id |
| Admin override | auth.uid() IN (SELECT user_id FROM admins) |
| Service-role only | (no policy — use service key on server) |
What RLS does not protect
- Server-side code using the service-role key. That key bypasses RLS by design. Never ship it to the client. Keep it in server-only routes.
- Admin dashboards. If you have an admin UI, it usually uses the service-role key — which means the admin auth check must happen separately in your code.
- Cron jobs and webhooks. These run server-side and typically bypass RLS. Check access logic explicitly.
- Storage buckets.Supabase Storage has its own policies. Set them; they don't inherit from the database tables.
The fastest way to verify your app
- Create two test accounts. Call them red and blue.
- Sign in as red. Create a piece of data. Note its ID.
- Open your browser's network tab. Copy the Supabase URL pattern the app uses.
- Sign out. Sign in as blue.
- Manually call the Supabase endpoint for red's data ID using blue's session.
- 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.