AI-coded apps

    Lovable App Security: Fixing the Exposed Supabase Service Role Key

    A Supabase Edge Function holding the service_role key as a secret, with a Lovable client using only the anon key.

    If Lovable put your Supabase service_role key in the client, it is a critical exposure that needs an immediate response and an architecture fix. The service_role key bypasses Row Level Security and gives full read and write access to your database, so anyone who extracts it from your frontend or repository has complete control. Rotate it right away, then fix the real problem: the service_role key must live server-side, in a Supabase Edge Function or your own backend, never in your Lovable app's client code. The client should use only the public anon key, protected by RLS.

    Short answer

    The service_role key is a server-side admin key, so the fix is to get it out of the client and keep it on a backend. Per Supabase's API keys documentation, it bypasses Row Level Security and has full data access. First rotate it, because the exposed copy is compromised. Then move any privileged operation into a Supabase Edge Function, where the key is available as a secret environment variable and never reaches the browser. The client keeps only the anon key, and RLS protects your data. Finally, remove the key from your code and Git history, and verify with the anon key that your tables are not exposed.

    Confirm it is the service_role key

    Before you act, confirm which key leaked, because the response is very different. Supabase gives you a public anon (publishable) key, which is meant to ship in the client, and a secret service_role key, which is a full-access admin key. If what leaked is the anon key, that alone is normal and your real concern is whether RLS is enabled. If it is the service_role key, treat it as an emergency.

    You can tell them apart by role: the service_role key is labeled as the secret or service role key in your Supabase dashboard, and it is the one that carries admin rights. If a Lovable-generated app is calling your database directly from the frontend with the service_role key, that is the exposure to fix, and everything below applies.

    Step 1: rotate the key immediately

    Rotating is the first move, because it is the only action that invalidates the copy an attacker may already have. In Supabase, with the newer API keys you revoke the specific secret key and issue a new one, and Supabase can automatically revoke secret keys it detects in public GitHub repositories. With the legacy keys, rotating the service_role key means rolling the JWT secret, which regenerates it and invalidates existing tokens.

    Rotate before you refactor. Even a perfect architecture fix does nothing about the key that already leaked, so kill the old key first, then rebuild the flow so it does not happen again. Assume the exposed key was captured, and review your database logs for unusual access around the time of the leak.

    Step 2: the real fix, move it server-side

    Rotation stops the bleeding; the real fix is architectural. The service_role key exists for privileged, server-side operations, so it must live on a server, not in an app you ship to users. The table below shows where each key belongs.

    KeyBelongs inNever in
    anon / publishableThe client, protected by RLS(it is safe to expose)
    service_role / secretA backend or Edge FunctionThe client, repo, or bundle
    JWT / signing secretSupabase, server-side onlyAnywhere client-side

    The principle is simple: anything shipped to a client can be extracted, so a secret in your Lovable frontend is not a secret. Privileged work moves to a backend the user cannot read, and the client is left with only the public key, which is safe because RLS governs it.

    Using a Supabase Edge Function to hold the service_role key

    A Supabase Edge Function is the natural place for the service_role key in a Lovable and Supabase app. Edge Functions run on Supabase's servers, and the service_role key is available to them as the SUPABASE_SERVICE_ROLE_KEY environment variable, which Supabase documents as safe to use there but never in a browser. Your client calls the function, the function performs the privileged operation with the key, and the key never leaves the server.

    The refactor is to identify every place your app used the service_role key directly, such as an admin write or a cross-user query, and move that logic into an Edge Function. The client then calls the function over HTTPS, ideally with the user's own auth token, and the function does the privileged work. This is the same backend-for-frontend pattern that protects any secret; Edge Functions just make it easy inside Supabase.

    Storing the key as a secret, not in code

    Even server-side, do not hardcode the key. Supabase provides secrets management for Edge Functions, through the CLI secrets commands or the Edge Function Secrets page in the dashboard, so keys and other secrets are stored securely and injected as environment variables at runtime. The built-in SUPABASE_SERVICE_ROLE_KEY is provided automatically, and any additional secrets you need go in the same secret store.

    Keeping the key in a managed secret, rather than in a file, means it never appears in your code or your repository, which is where leaks start. It also makes rotation cleaner, because you update the secret in one place instead of hunting through source. Treat the secret store as the only home for the key, and keep it out of both the client and the codebase.

    The client uses only the anon key, with RLS

    With the service_role key server-side, your client uses only the public anon key, and Row Level Security is what makes that safe. RLS is off by default on new tables, so you must enable it on every table and write policies matching your auth logic, such as letting each user read only their own rows. Then the anon key, even though it is public, can only reach data your policies allow.

    This is the whole security model of a Supabase client app, and it is what a Lovable-generated project often skips. The client with the anon key and RLS handles normal operations; the Edge Function with the service_role key handles the privileged ones. Get this division right and the exposed-key problem cannot recur, because there is no secret in the client to expose.

    Remediation checklist

    Work the fix in order, from stopping the leak to verifying it is closed. The checklist below covers it.

    StepActionWhat it confirms
    1Rotate the exposed service_role keyThe old key is dead
    2Move privileged calls to an Edge FunctionThe key leaves the client
    3Store the key as a Supabase secretIt is not hardcoded
    4Enable RLS so the client uses only anonData is protected by policy
    5Remove the key from code and Git historyNo re-leak of the old key
    6Query your tables with the anon keyThe fix is verified

    The two steps people skip are the last two: removing the key from Git history, since a later commit does not erase earlier ones, and actually testing with the anon key that your data is protected. Both are quick and both are what confirm the exposure is truly closed.

    Verify the fix

    After remediation, verify rather than assume. Confirm the old service_role key is revoked and no longer works, check that no client file or the shipped bundle contains the key, ensure RLS is enabled on every table, and then run the attacker's own test: query your tables with the public anon key and confirm it returns only what your policies allow.

    That last check is the direct proof. A scanning bot or an attacker will try exactly that, so if your anon key cannot reach data it should not, and the service_role key is gone from the client and rotated, the exposure is closed. Keep the verification as a step you repeat after future changes, because a new privileged call could reintroduce the same mistake.

    Scan the build before you ship

    Because AI builders like Lovable make it easy to ship a secret by accident, checking the build before release catches a committed or bundled service_role key while it is still cheap to fix. A key in the frontend is exactly the pattern automated attackers hunt for, so finding it first keeps you ahead of them.

    A scanner like PTKD.com analyzes your mobile build and returns findings ordered by severity and mapped to OWASP MASVS, including embedded secrets and insecure network and storage patterns, so an exposed key does not reach users. To be clear about the boundary: a build scanner checks the app you ship, not your Supabase dashboard, so it does not replace moving the key to an Edge Function, rotating it, or enabling RLS. It catches the client-side exposure; the server-side architecture is yours to fix.

    What to take away

    • An exposed Supabase service_role key in a Lovable app is critical: it bypasses RLS and gives full database access.
    • Rotate the key first, because the exposed copy is already compromised.
    • Move the service_role key server-side, into a Supabase Edge Function, and store it as a secret, never in client code.
    • Leave the client with only the anon key, and enable RLS on every table so that key is safe.
    • Scan each build with PTKD.com so a service_role key never ships to users again.
    • #lovable
    • #supabase
    • #service_role key
    • #edge functions
    • #row level security

    Frequently asked questions

    What is the Supabase service_role key and why is it dangerous in Lovable?
    The service_role key is a secret, server-side admin key that bypasses Row Level Security and has full read and write access to your database. It is dangerous in a Lovable app when the generated code calls the database directly from the frontend with it, because anyone can extract it from the client and gain complete control. It should never leave your server.
    How do I rotate a leaked service_role key?
    In Supabase, with the newer API keys you revoke the specific secret key and issue a new one, and Supabase auto-revokes secret keys found in public GitHub repos. With legacy keys, rotating the service_role key means rolling the JWT secret, which regenerates it and invalidates existing tokens. Rotate first, then remove the key from your code and Git history.
    Where should the service_role key live instead?
    On a server, never in the client. In a Supabase app, the natural home is a Supabase Edge Function, where the key is available as a secret environment variable and never reaches the browser. Your client calls the function to perform privileged operations, so the key stays server-side while the frontend keeps only the public anon key.
    How do I use it in a Supabase Edge Function?
    Edge Functions run on Supabase's servers and receive the service_role key as the SUPABASE_SERVICE_ROLE_KEY environment variable, which is safe to use there but never in a browser. Move any privileged logic into a function, store any extra secrets through Supabase's secrets management rather than hardcoding them, and have your client call the function over HTTPS.
    Does the client need the service_role key at all?
    No. A correctly built Supabase client app uses only the public anon key, with Row Level Security enforcing what it can access. Privileged operations that need to bypass RLS run in an Edge Function or backend with the service_role key. If your client seems to need the service_role key, that logic belongs on the server instead.
    How do I confirm the exposed key is not in my build?
    Check that no client file or shipped bundle contains the key, and scan the build to be sure. A scanner like PTKD.com (https://ptkd.com) analyzes your .apk or .ipa for embedded secrets and insecure patterns, mapped to OWASP MASVS. It checks the app you ship, not your Supabase settings, so pair it with rotating the key and enabling RLS on the server side.

    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