Hire Lovable Xperts
Security

Lovable RLS & Auth: Getting Access Control Right

Row-Level Security is the single most important security control in a Supabase-backed Lovable app, and also the most commonly misconfigured one. This guide covers the engineering facts: how RLS evaluation works, the four-policy CRUD template every Lovable app needs, how to fix the 'Infinite recursion detected in policy' error, and the precise boundary between the anon key and the service_role key. No theory — only what you need to configure it correctly.

By Founder Name · Last verified: 2026-06-23

How does Supabase RLS actually work?

Supabase Row-Level Security runs inside PostgreSQL, evaluated per row before any data leaves the database. When a client queries using the anon key, Supabase attaches the user's JWT to the request context. The RLS policy evaluates auth.uid() — the user ID from that JWT — against each row. If the expression returns false, that row is invisible to the query as if it does not exist.

This means RLS is not optional extra hardening — it is the only thing standing between one user's data and another's when your app uses the client-side Supabase SDK. The anon key, by design, respects RLS. The service_role key, by design, bypasses it. Which key your client uses determines whether your RLS policies run at all.

What does a complete four-policy RLS template look like?

The simplest and safest pattern for most Lovable apps is a standard set of four policies per user-data table: SELECT limited to the owner's rows, INSERT limited to the owner's user_id, UPDATE limited to the owner's rows, and no DELETE policy unless the feature explicitly requires it. This pattern is correct for the vast majority of Lovable use cases where each user owns their own data.

  1. Enable RLS: ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
  2. SELECT policy: CREATE POLICY 'users_select_own' ON your_table FOR SELECT USING (user_id = auth.uid());
  3. INSERT policy: CREATE POLICY 'users_insert_own' ON your_table FOR INSERT WITH CHECK (user_id = auth.uid());
  4. UPDATE policy: CREATE POLICY 'users_update_own' ON your_table FOR UPDATE USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());
  5. Test each policy: in the Supabase SQL editor run SET LOCAL role = authenticated; SET LOCAL request.jwt.claims = '{"sub": "<user-uuid>"}'; then run your query.
auth.uid() is re-evaluated per row, per query — it is not cached for a session. This means the policy correctly handles cases where user_id changes between requests, at the cost of a small per-row evaluation overhead that is negligible for typical Lovable app data volumes.
Four-policy CRUD template per user-data table
OperationPolicy nameSQL pattern
SELECTusers_select_ownFOR SELECT USING (user_id = auth.uid())
INSERTusers_insert_ownFOR INSERT WITH CHECK (user_id = auth.uid())
UPDATEusers_update_ownFOR UPDATE USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid())
DELETEusers_delete_own (add only if needed)FOR DELETE USING (user_id = auth.uid())

What causes 'Infinite recursion detected in policy'?

This error appears when an RLS policy queries the same table it is protecting. A common example is a policy on a profiles table that runs SELECT from profiles to determine whether the current user is an admin. Supabase must evaluate the policy before allowing the query, but evaluating the policy requires running a query against the same table, which triggers the same policy again — an infinite loop. The table locks until the policy is corrected.

  1. Identify the recursive policy: SELECT * FROM pg_policies WHERE tablename = 'your_table' AND qual LIKE '%your_table%';
  2. Rewrite the policy to use JWT claims instead of a subquery. Store the user's role in the JWT at sign-in rather than looking it up during query evaluation.
  3. If admin status must be looked up at query time, use a separate roles table with its own non-recursive SELECT policy, and reference that in your main table's policy with SECURITY DEFINER to avoid the recursion.
  4. After rewriting, verify the fix: run a simple SELECT in the SQL editor under the affected role to confirm the recursion error is gone.
  5. Test with a non-admin user and an admin user separately to ensure both roles behave correctly.
Infinite recursion in an RLS policy locks the entire table — reads and writes fail for all users until fixed. If this error appears in production, go directly to the Supabase SQL editor to rewrite the policy. Do not attempt to fix it from the Lovable editor.

What is the difference between the anon key and the service_role key?

The anon key is the public client key: it is safe to include in browser-side JavaScript, it respects all RLS policies, and it is what the Supabase client constructor should use in client-side code. The service_role key bypasses all RLS — it can read, write, and delete any row in any table without restriction. It is intended for trusted server-side operations only: edge functions, server actions, background jobs. It must never appear in client-side code.

Independent researchers have documented that Lovable-generated apps can ship the Supabase service_role key — which bypasses Row-Level Security — into the client-side JavaScript bundle; one April 2026 audit of 50 apps found this in 34% of them (source: Tomer Goldstein, DEV.to, April 2026). If a user opens your app's network traffic or the bundled JS, they can extract that key and query your entire database with no access controls. Search your repo for 'service_role' and 'SUPABASE_SERVICE_ROLE_KEY' to confirm it does not appear in any client-side file.

When should I bring in an expert to review my RLS setup?

RLS mistakes are silent: they do not cause build errors, do not appear in the Lovable preview, and do not surface until a user (or an attacker) queries data they should not be able to see. If your app handles payment data, health information, messages between users, or any personally identifiable data, a professional RLS audit before launch is the only reliable way to confirm the policy set is correct and complete.

An audit checks more than whether RLS is enabled — it tests whether the policies are logically correct, whether there are any tables that were accidentally left unprotected, whether your edge functions use the correct key for each operation, and whether your auth guards on the front-end routes match the database-level restrictions. Book a call and we can scope the review for your app's specific data model.

Frequently asked questions

Why can Lovable users see each other's data?
The most common cause is that RLS is disabled on the table, or the SELECT policy is missing. With RLS off, every authenticated user can read every row. Enable RLS on the table in Supabase and add a SELECT policy: CREATE POLICY 'users_select_own' ON your_table FOR SELECT USING (user_id = auth.uid()). This limits each user to reading only rows they own.
What's the difference between the anon key and the service_role key?
The anon key is safe in the browser — it respects RLS policies. The service_role key bypasses all RLS — it can read and write any row in any table. The service_role key must only be used in server-side code (edge functions, server actions) and must never appear in client-side JavaScript. If it does, any user can extract it and query your database with no restrictions.
How do I know if my Lovable app is secure?
Start by verifying RLS is enabled on every table in Supabase and that each table has correct SELECT and write policies. Then search your codebase for 'service_role' to confirm the key never reaches the client. For a complete answer, a security audit tests your full data flow — policies, edge functions, auth guards, and secret handling — and produces a written report of what is correct and what needs fixing.
Can auth.uid() be spoofed by a client?
No. auth.uid() reads the user ID from the JWT that Supabase issues at sign-in. The JWT is signed with your project's secret key, and Supabase validates the signature before evaluating RLS policies. A client cannot forge a JWT with a different user_id without access to your project's signing secret.
What is the 'infinite recursion' error in Supabase RLS?
It occurs when an RLS policy queries the same table it is protecting — typically to look up a user's role. Supabase tries to evaluate the policy, which triggers a query, which triggers the policy again. The fix is to use JWT claims for role checks instead of a subquery, or to use a separate roles table with a non-recursive policy and SECURITY DEFINER access.
Do I need a DELETE policy on every table?
No. If your application does not expose a delete action for a given resource, you should not add a DELETE policy — it reduces your attack surface. The four-policy template deliberately makes DELETE opt-in. If you do need delete functionality, add FOR DELETE USING (user_id = auth.uid()) to limit deletion to rows the current user owns.
What happens if I enable RLS but don't add any policies?
If RLS is enabled on a table but no policies are defined, the table defaults to deny-all — no user can read or write any row, including the owner. This is safer than having RLS off, but it will break your app until you add the correct policies. Enable RLS and add policies in the same SQL transaction to avoid a gap where your app is broken in production.
Can RLS protect against a compromised service_role key?
No. The service_role key is specifically designed to bypass RLS entirely. If the service_role key is compromised, an attacker can query your database with no RLS restrictions, regardless of what policies you have configured. This is why the service_role key must never appear in client-side code — RLS cannot protect against it once that key is extracted.
How do I test that my RLS policies are correct?
In the Supabase SQL editor, set the session role and JWT claims manually: SET LOCAL role = authenticated; SET LOCAL "request.jwt.claims" = '{"sub": "<user-uuid>"}'; then run the query you want to test. Try with two different user UUIDs and verify that each user can only see their own rows. Also try with role = anon to verify unauthenticated users are blocked.

Talk to a senior engineer — not a salesperson.

Book a free 30-minute audit call. We'll diagnose what's wrong and tell you exactly what it costs to fix.

Book a free audit call