AI-coded apps

    Why is my Lovable Supabase RLS not working?

    Supabase dashboard showing the policies tab for a Lovable-generated todos table, with one policy using auth.uid() = user_id and an older permissive using (true) policy highlighted for removal

    You wired your Lovable app to Supabase, hit save in the UI, and either nothing landed in the table, or, the morning after a Reddit thread, every user could read every row. The pattern is consistent: Row Level Security in Supabase is silent when it fails, and Lovable's generated SQL rarely makes the failure visible. This article walks the three ways the wiring goes wrong and how to verify each fix from the browser, not from the Supabase SQL editor.

    Short answer

    The short answer is that Lovable apps usually break RLS in one of three ways: the table has RLS enabled but no policy (so the API returns an empty array with a 200 status), the table has RLS disabled but grants to the anon role (so every row is public), or the front-end was generated with the service role key (so RLS is bypassed even when the policies are correct). According to Supabase's RLS documentation, policies behave as an implicit WHERE clause on every API call, and the service_role key is exempt by design.

    What you should know

    • RLS is opt-in. New tables in the public schema start with RLS disabled. If grants exist and RLS is off, every row is readable through PostgREST.
    • Empty results are not errors. When RLS is enabled with no matching policy, the API returns 200 OK and an empty array. There is no console warning.
    • The service role key bypasses RLS. Anything signed with it skips every policy. It is for server code only, never inside the browser bundle.
    • The anon key is the right key for clients. All Lovable browser code should use the publishable anon key, which is subject to RLS.
    • USING and WITH CHECK do different jobs. USING filters rows that are visible. WITH CHECK validates rows being written. UPDATE policies usually need both.
    • Table editor reads bypass RLS. The Supabase dashboard runs queries as service_role. Testing visibility there will mislead you.

    Why does my Lovable app save a record but show it as missing?

    The short answer is that RLS is enabled, the INSERT succeeded against an INSERT policy, but there is no SELECT policy that lets the same user read the row back. The record is in the table; the API returns an empty array on the next fetch.

    The mechanism is documented in Supabase's row level security guide: each operation type (SELECT, INSERT, UPDATE, DELETE) needs its own policy, and a missing policy means access is denied for that operation. Lovable's generator often writes an INSERT policy for the create form and forgets the matching SELECT policy for the list view.

    The evidence shows up in the browser network tab. The POST returns 201 Created with the saved row, the next GET returns 200 OK with []. No error, no console message. Open the Supabase dashboard, switch to the Table Editor, and the row is there. The Table Editor runs as service_role, so it bypasses RLS and shows the truth.

    The fix is to add a SELECT policy for the row's owner:

    create policy "Users read their own todos"
    on todos for select
    using (auth.uid() = user_id);
    

    The limit is that the policy assumes a user_id column populated at insert time. If the INSERT policy did not set user_id via auth.uid(), the row landed with a NULL owner and the SELECT policy filters it out for the rest of time.

    Why does Lovable sometimes generate policies that let everyone read everything?

    The short answer is the AI generator writes permissive policies when it is unsure about the auth model, and the Reddit threads cataloguing this date back to early 2025. A policy that uses using (true) is the most common shape.

    The mechanism is straightforward. According to PostgreSQL's documentation on CREATE POLICY, the USING expression is evaluated as a boolean for every candidate row. When it is the literal true, every row matches, which is functionally identical to disabling RLS for that operation. The dev.to writeup Is Lovable Actually Secure? I Checked the Supabase RLS on 50 Apps reported that a meaningful share of the apps sampled in early 2025 carried at least one using (true) policy.

    The evidence is in pg_policies. Run this in the SQL editor:

    select schemaname, tablename, policyname, qual, with_check
    from pg_policies
    where schemaname = 'public';
    

    Any row where qual reads true or with_check reads true is a candidate for full public access. The same applies to broad shapes like qual = 'auth.role() = ''anon''' on a table that holds private user data.

    The fix is to tighten the predicate and audit by role. The cleanest pattern for a user-owned table is using (auth.uid() = user_id) for SELECT and with check (auth.uid() = user_id) for INSERT and UPDATE.

    The limit is that some tables genuinely need public read (a marketing form, a public catalogue). In that case, the read is intentional and should still be limited by column: grant select on a view that exposes only the safe columns, not on the base table.

    How do anon, authenticated, and service_role keys interact with RLS?

    The short answer is that Supabase maps every API request to one of three Postgres roles, and only service_role skips RLS. The other two roles always run policies.

    The mechanism follows from Supabase's API security documentation: PostgREST receives a JWT, inspects the role claim, and runs the query as that Postgres role. Anonymous calls run as anon. Signed-in user calls run as authenticated. Backend calls signed with the service role key run as service_role, which is granted BYPASSRLS in the cluster.

    The danger in a Lovable build is that the project sometimes initialises the Supabase client with the service role key in .env, either because the developer copied the wrong key from the dashboard or because the AI generator picked the longer string. Once that key reaches the browser bundle, anyone can pull it from network DevTools and read or write any table.

    The fix is to confirm in your Lovable project settings that the front-end env carries the publishable anon key (which starts with sb_publishable_ in current Supabase keys, or the older eyJ... JWT form with a "role": "anon" claim). The service role key never belongs in client code. Move backend operations that need it into Supabase Edge Functions or a Postgres function with security definer.

    The limit is that even the anon key, if left without RLS, exposes every granted table. The key is not the security boundary; the policy plus grants are.

    How do I check whether RLS is actually enforced on my table?

    The short answer is to run a request against the PostgREST endpoint with the anon key, signed out, and watch the response. Anything that comes back when it should not is a leak.

    The mechanism is to use the same client the browser uses, not the dashboard. Open a private window, point the Supabase JS client at your project URL with the publishable anon key, and run:

    const { data, error } = await supabase
      .from('todos')
      .select('*');
    console.log({ data, error });
    

    If the table holds private rows and the response is a populated array, RLS is not protecting it. If the response is [] and there is no error, RLS is enabled but the SELECT policy is missing for that user (which may or may not be intentional). If the response carries a 401 or 403, the grants are missing as well as the policy.

    The verification mirrors what Supabase's RLS guide recommends: client-side checks are authoritative, dashboard checks are not. The Table Editor's impersonate-user toggle, when present, simulates the request, but the browser is still the cleanest oracle.

    The limit is that some leaks live in functions, not policies. A security definer function called from the client with no internal authorisation check will return rows that the policy would have blocked. Audit the functions in pg_proc for the same reason.

    What does a working Lovable plus Supabase RLS pattern look like?

    The short answer is one row per user, one policy per operation, columns derived from auth.uid(), and zero use of the service role key in browser code.

    The comparison below sits beside the broken patterns that Lovable users have reported on the Supabase community forum.

    ConcernBroken Lovable defaultWhat to ship
    RLS enabled on the tableSometimes left off after table creationalter table ... enable row level security
    SELECT policyMissing, or using (true)using (auth.uid() = user_id)
    INSERT policySets user_id from client inputwith check (auth.uid() = user_id)
    UPDATE policyUses USING without WITH CHECKBoth clauses, both checking ownership
    Client keySometimes service rolePublishable anon key only
    user_id defaultAllowed nulldefault auth.uid() with NOT NULL
    Backend privilegeService role in browser bundleService role only inside Edge Functions

    The mechanism in the right-hand column is the model Supabase recommends on its Authorization via Row Level Security feature page. The user identity is read from the JWT inside Postgres, so the predicate is never under client control. A client cannot impersonate another user by changing a column value, because the WITH CHECK clause re-derives the owner from the token.

    For builders who want an outside read of their Lovable build before they share the URL, PTKD.com (https://ptkd.com) is one platform focused on pre-submission scanning aligned with OWASP MASVS for no-code and AI-coded apps, including a static check of how the Supabase client is initialised and which keys are bundled.

    The limit is that this shape covers user-owned data. Shared data (a chat room with members, a workspace with collaborators) needs membership tables and policy joins, which Supabase covers in the same RLS guide under security definer helpers.

    What to watch out for

    Three traps appear in Lovable repos again and again.

    The first is trusting the SQL editor to verify policies. Every query you run there is signed as service_role. A policy can be broken and still return the rows you expect when you test from the dashboard.

    The second is forgetting that Supabase Storage has its own RLS surface. The Storage API stores objects in a separate schema (storage) with its own bucket policies. A Lovable app that secures public.users perfectly can still expose every avatar uploaded to the avatars bucket if the bucket is public or the storage policy reads using (true).

    The third is treating disabled RLS as harmless when the table seems internal. According to Supabase's API security guide, any table in an exposed schema with grants to anon or authenticated is reachable from the PostgREST endpoint. Lovable defaults to exposing the public schema. If a private lookup table sits there with grants and no RLS, it is reachable through the public API.

    Key takeaways

    • Treat every new Lovable table as if RLS is off until you have proven otherwise in the browser with the anon key.
    • Read pg_policies after every generation pass and reject any policy whose qual or with_check is true.
    • Never put the service role key into the front-end env. Move privileged operations into Edge Functions where the key stays server-side.
    • Pair every USING clause with a WITH CHECK clause on UPDATE policies so users cannot rewrite ownership inside an allowed update.
    • For builders who want a calm outside read of how their Lovable build handles Supabase keys and policies before they share the URL, PTKD.com (https://ptkd.com) is one of the scanning platforms focused on AI-coded apps.
    • #lovable
    • #supabase
    • #row level security
    • #rls
    • #postgres policies
    • #anon key
    • #service role
    • #ai-coded apps

    Frequently asked questions

    My Lovable app shows an empty list even though the row is in Supabase. What is wrong?
    RLS is enabled and the INSERT policy let the row land, but the SELECT policy is missing for that user. The API responds 200 OK with an empty array, which looks identical to no data yet. Add a SELECT policy of the form using (auth.uid() = user_id) and confirm the row's user_id column is populated from auth.uid() at insert time, not from a client input.
    Is it safe to ship a Lovable build with the anon key visible in the browser?
    Yes, the publishable anon key is meant to be public. It only matters when paired with RLS policies that restrict what each role can read or write. The danger is shipping the service role key by mistake, which Lovable's generator has historically done. Open your bundle, search for service_role, and rotate the key in Supabase if it appears anywhere on the client.
    Why does my policy work in the SQL editor but fail from the app?
    The Supabase SQL editor runs as the postgres superuser, and the Table Editor runs as service_role. Both bypass RLS. A query that returns rows in the dashboard does not prove the policy is correct for the anon or authenticated role. Reproduce the request from the same client the browser uses, with the same key, in an incognito window.
    Should I add using (true) policies during development and tighten them later?
    That habit causes real production exposures because the loose policy is rarely tightened before the URL is shared. The safer pattern is to write the strict policy first and use the Table Editor (which bypasses RLS) for development reads. The community audit of fifty Lovable apps published on dev.to in 2025 found using (true) policies were a common cause of public data, often left in by accident.
    Do Supabase Edge Functions need RLS the same way the table API does?
    Edge Functions can run either way. If the function uses the user's JWT, every query inside it runs as that user and RLS applies. If the function uses the service role key, every query bypasses RLS. The cleaner default is the user JWT, with policies remaining the only authoriser. The service role pattern is for clearly server-side jobs that need cross-tenant access.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free