# My Supabase public key is visible. Is that a problem?

Checked on 1 November 2026

No. The Supabase public key, also called the anon or publishable key, is made to sit in your app’s browser code, and [Supabase’s own docs](https://supabase.com/docs/guides/getting-started/api-keys) call it safe to expose.

The real risks are a table without row level security (database rules that decide who may read or change each row) or with a rule that lets everyone in, because the public key can then read it, and a secret or service_role key in your browser code, because it skips every rule. Check every table with Supabase’s Security Advisor, and replace any secret key you find in your live code with a new one.

On 1 November 2026, LidTight’s free check on Tidybook, our demo app built with Lovable, found no secret keys in the code, yet anyone could read its reviews table: 12 rows with first names, emails and phone numbers.

## Why the public key is meant to be seen

Every request to your Supabase project carries an API key (a code that tells Supabase which app is calling). Your app’s browser code needs one to reach the database, and anyone can read browser code. So Supabase gives you a public key for this job. [Its docs](https://supabase.com/docs/guides/getting-started/api-keys) picture it as a key “taped to the front door” that only opens the lobby.

What keeps the rest of the building shut is [row level security](https://supabase.com/docs/guides/database/postgres/row-level-security), or RLS. A request with the public key and no sign-in runs as the **anon** role (Supabase’s name for a visitor who is not signed in). A signed-in user runs as **authenticated**. Your RLS rules say what each role may read, add, change and delete. A table with no rules is the problem, not the key.

## The four kinds of Supabase key

Supabase’s [API keys page](https://supabase.com/docs/guides/getting-started/api-keys) lists two current keys and two older (“legacy”) ones:

| Key | What it looks like | Where it belongs | What it can reach |
| --- | --- | --- | --- |
| Publishable key | Starts with **sb_publishable_** | Browser, mobile app, any code you ship | Only what your RLS rules allow |
| anon key (legacy) | A long code that starts with **eyJ** | Same as the publishable key | Only what your RLS rules allow |
| Secret key | Starts with **sb_secret_** | Only server code you control, never the browser | All your data. It skips every RLS rule |
| service_role key (legacy) | A long code that starts with **eyJ** | Same as the secret key | All your data. It skips every RLS rule |

Watch the names in your settings file (.env). A name that starts with VITE_ or NEXT_PUBLIC_ goes into the browser code. [Supabase warns](https://supabase.com/docs/guides/getting-started/api-keys) never to give a server-only key such a name, and [Lovable’s docs](https://docs.lovable.dev/features/secrets) say VITE_SUPABASE_PUBLISHABLE_KEY is safe to be public.

> Supabase is deprecating (phasing out) the anon and service_role keys by the end of 2026, and asks you to use publishable and secret keys instead ([API keys](https://supabase.com/docs/guides/getting-started/api-keys)). If your app still uses a key that starts with eyJ, plan the move now.

## A real example: a safe key and an open table

On 1 November 2026, LidTight ran its free check on Tidybook, our own demo app built with Lovable. Its database is Lovable Cloud, Lovable’s built-in backend, which [is built on Supabase](https://docs.lovable.dev/features/cloud). The check read the 27 files (738 KB) of code that every visitor downloads. These lines are from that run:

- “Looking for secret keys: none found”
- “CRITICAL Anyone can read your reviews table: 12 rows, with first_name, email, phone”
- “No sign-in needed. A stranger can download these records with one request.”

The public key was in the code, as it should be, and the check did not report it. Of the two tables, bookings and reviews, only reviews was open to strangers (“1 open to anyone”). Same key, two tables, two results. The rules made the difference, not the key.

Reviews are often meant to be public. Email addresses and phone numbers are not. RLS decides which rows someone can read, [not which columns](https://supabase.com/docs/guides/database/postgres/column-level-security). So a table that strangers may read should hold only details you are happy to publish. Keep email and phone in a separate table with its own rules.

## Check which key your live app ships

1. Open your live app, not your project files, in Chrome. Click through a page that shows data, so its code loads.
2. Open Chrome’s developer tools: Cmd + Option + I on a Mac, Ctrl + Shift + I on Windows ([Chrome docs](https://developer.chrome.com/docs/devtools/open)).
3. Open the search across all loaded files: Cmd + Option + F on a Mac, Ctrl + Shift + F on Windows ([Chrome docs](https://developer.chrome.com/docs/devtools/search)).
4. Search for **sb_secret_**. A match followed by a long random code is a leaked secret key. Go to “If a secret key leaked” below. The bare text sb_secret_ with nothing after it is not a key.
5. Search for **sb_publishable_**. A match followed by a long code is your public key. That is expected.
6. Search for **eyJ**. Both legacy keys start this way, so you cannot tell them apart by eye. Compare each long match with the keys under **Settings > API Keys** in your Supabase dashboard. The anon key is fine. The service_role key is a leak.

The [free check](https://lidtight.com/#check-now) on this site does the same search in your live app’s code. It looks for Supabase secret keys and reads the role written inside each legacy key, so it can tell an anon key from a service_role key. It does not report public keys, because they belong there.

## Check that every table has rules

With your own Supabase project, use Supabase’s [Security Advisor](https://supabase.com/docs/guides/observability/advisors), which is built into Supabase. It checks your database setup for common problems. Supabase notes that some findings may be intentional, so read each one.

1. Open the [Security Advisor](https://supabase.com/dashboard/project/_/advisors/security) in your Supabase dashboard.
2. Fix every “Table publicly accessible” finding. It means RLS is off on a table in the public schema (the part of the database your app’s API exposes). Supabase says that anyone with your project address can then read, edit and delete all data in that table.
3. Fix every “Sensitive data publicly accessible” finding: a table with RLS off and columns whose names look like passwords, tokens or ID numbers. Its list of names does not include email or phone, so do not count on it for those.
4. Read every “Security policy allows unrestricted access” warning. It means a rule such as USING (true) lets everyone in that role reach every row. Keep such a rule only for data that is meant to be public.

On Lovable Cloud, open **More > Database**, select **Manage**, then **Policies** ([Lovable docs](https://docs.lovable.dev/features/database-access)). Each rule shows its action (SELECT means reading), who it covers (anon means visitors who are not signed in) and its condition. To change a rule, ask Lovable in the project chat.

To see the result as a stranger would, run the [free check](https://lidtight.com/#check-now). It counts the rows a stranger can read in each table and keeps none of them. It cannot see your rules, and it does not test what a signed-in user can read: see [Can one customer see another customer’s data?](https://lidtight.com/guides/one-customer-sees-another)

## Close an open table

In the SQL Editor of your Supabase dashboard, turn on RLS for the table. [Supabase’s guide](https://supabase.com/docs/guides/database/postgres/row-level-security) explains that the public key then reads nothing from it until you add rules. Then add one rule per action: read, add, change, delete. This example, based on the guide, lets each signed-in user read only their own profile:

```
-- Turn on row level security
alter table public.profiles enable row level security;

-- A rule for reading: each signed-in user sees only their own row
-- (the table needs a user_id column with the owner id)
create policy "Users can view their own profile"
on public.profiles for select
to authenticated
using ( (select auth.uid()) = user_id );
```

The guide also asks you to take back the default grants (permissions each role gets on a new table), because rules do not remove them. It shows how to test each table too.

On Lovable, ask in the project chat, for example: “Turn on row level security for every table. Add a rule for each action: who may read, add, change and delete. Keep customers’ email and phone numbers in a table that strangers cannot read.” Then check again.

## If a secret key leaked: rotate it

A secret or service_role key skips every RLS rule, and [Supabase says](https://supabase.com/docs/guides/getting-started/api-keys) that exposing one puts all of your project’s data at risk. The newer secret keys refuse to work in a browser, but an attacker can still use them from other tools. So deleting the key from your code is not enough. You must rotate it: make a new key and turn the old one off. Supabase’s [rotation guide](https://supabase.com/docs/guides/security/rotate-leaked-api-keys) gives these steps:

1. Fix the cause first. Move the code that uses the key to your server or an Edge Function (server code that never reaches the browser), so the new key cannot leak the same way. If someone is already using the leaked key, retire it at once (step 5).
2. Create a new secret key under **Settings > API Keys** in your Supabase dashboard.
3. Put the new key everywhere your server code used the old one.
4. If the leaked key was a legacy service_role key, also switch your browser code from the anon key to a publishable key. Retiring the leaked key turns off both legacy keys.
5. Retire the leaked key: delete a secret key, or deactivate the legacy keys. Deleting cannot be undone.
6. For a legacy service_role key, also revoke the legacy JWT secret (the value that signed the legacy keys). Supabase’s guide links to the steps.

On Lovable Cloud, Lovable manages the SUPABASE_ keys, and its Secrets screen cannot change them ([Lovable docs](https://docs.lovable.dev/features/secrets)). If one is in your browser code, ask Lovable in the project chat to move that code to an Edge Function, and ask Lovable support how to replace the key.

For a leaked AI key, see [Someone is running up your AI bill](https://lidtight.com/guides/ai-bill-abuse). For everything to check before launch, see the [launch security checklist](https://lidtight.com/guides/launch-security-checklist).

## Sources

1. [Supabase: API keys](https://supabase.com/docs/guides/getting-started/api-keys)
2. [Supabase: Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security)
3. [Supabase dashboard: Security Advisor](https://supabase.com/dashboard/project/_/advisors/security)
4. [Supabase: Advisors (Security Advisor checks)](https://supabase.com/docs/guides/observability/advisors)
5. [Supabase: Rotate a leaked or compromised API key](https://supabase.com/docs/guides/security/rotate-leaked-api-keys)
6. [Supabase: Column Level Security](https://supabase.com/docs/guides/database/postgres/column-level-security)
7. [Lovable: Built-in backend (Cloud)](https://docs.lovable.dev/features/cloud)
8. [Lovable: Secrets, and VITE_ environment variables](https://docs.lovable.dev/features/secrets)
9. [Lovable: Control who can access your app’s data](https://docs.lovable.dev/features/database-access)
10. [Chrome DevTools: Open Chrome DevTools](https://developer.chrome.com/docs/devtools/open)
11. [Chrome DevTools: Search across all loaded resources](https://developer.chrome.com/docs/devtools/search)
