The short answer
Don’t give an AI agent your Supabase service_role key or a new-style secret key. Supabase’s secret keys authorize requests through the built-in service_role Postgres role, which has the BYPASSRLS attribute, so it skips every Row Level Security policy on every table.1 Your policies are the thing that limits what a client can reach. With that key, the agent meets none of them.
Give the agent its own narrow Postgres role or a user-scoped token instead, and keep the secret key in server code you wrote and reviewed.
Deedglass
A database change, checked and put back
-
1The agent’s change
Change order 42
UPDATE orders … WHERE id = 42
The agent: “1 row updated” -
2What the database really did
301 rows
A hidden trigger changed 300 more
Doesn’t match -
3Put back
301 rows restored
Signed off by a person
Matches
What the key actually is
Supabase hands out two kinds of key. Publishable keys (and the legacy anon key) are meant to be public: anyone can read one, so it only reaches what Row Level Security allows. Secret keys (and the legacy JWT service_role key) are the opposite. Supabase’s docs describe them as giving full access to a project’s data, and say never to put one in a browser, a shipped application or source control.1
Two details matter for agents:
- A secret key bypasses RLS only when the request carries no user access token. If a request also carries a signed-in user’s token, that user’s policies apply.2 A backend script or an agent tool that sends just the key gets the full bypass.
- Supabase blocks secret keys in browsers. It matches on the
User-Agentheader and answers 401.1 That check protects web pages. It does nothing for a coding agent, an MCP server or a script running in a terminal, which is exactly where agents run.
Supabase is retiring the legacy anon and service_role keys by the end of 2026, and creating new keys doesn’t revoke the old ones: they stay valid until you disable them.1 So a project can have both a new secret key and an old service_role JWT live at the same time, and an agent may hold either.
The mechanism in plain Postgres
Nothing here is special to Supabase. In PostgreSQL, superusers and roles with BYPASSRLS always bypass row security, and table owners do too unless the table has FORCE ROW LEVEL SECURITY.3 We ran this on a local PostgreSQL 18.6 to see it, with one table, one policy and two roles:
Code A database command in SQL. The text around it says what it does.
create role app_user nologin;
create role svc nologin bypassrls;
create table notes(id int primary key, owner text, body text);
insert into notes values (1,'alice','a1'),(2,'bob','b1'),(3,'bob','b2');
alter table notes enable row level security;
create policy own on notes using (owner = current_setting('app.uid', true));
grant select on notes to app_user, svc;
set app.uid = 'alice';
set role app_user;
select 'table as app_user' as q, count(*) from notes; -- the policy applies
reset role;
set role svc;
select 'table as bypassrls role' as q, count(*) from notes; -- BYPASSRLS
The two results, as psql printed them:
Code What the screen or file shows, copied as is.
q | count
-------------------+-------
table as app_user | 1
q | count
-------------------------+-------
table as bypassrls role | 3
Same table, same session setting, same grant. The role with BYPASSRLS saw all three rows. That is what an agent holding the secret key sees on every table you protected with a policy.
Where agents pick up the key
The key rarely gets handed to an agent on purpose. It arrives by convenience:
| How it happens | Why it’s easy to miss |
|---|---|
SUPABASE_SERVICE_ROLE_KEY sits in .env, and a coding agent reads the file or runs code that loads it |
The agent never prints the key; it just uses it |
| An MCP server or tool is configured with the secret key “so it works” | Every tool call now runs past RLS |
| A seed or admin script in the repo uses the key, and the agent runs it to “fix” data | The script was written for a person running it once |
| Edge Function code reads the key from the environment | Supabase’s docs advise against reading the key from the environment inside an Edge Function1 |
The July 2025 Supabase MCP demonstration used this exact shape: an assistant connected with a service_role credential read a table it should never have reached. Our write-up of that demo goes through the primary sources.
What to give the agent instead
Pick the narrowest option that still does the job:
- The publishable key plus a real user’s session. The agent acts as that user, and that user’s policies apply. Good for agents acting on behalf of one person.
- A dedicated Postgres role. Create a role for the agent with only the grants it needs, no
BYPASSRLS, and policies written for it. Supabase’s own docs warn never to share login credentials for any role with the bypass RLS privilege.2 Our post on safe Postgres access for agents shows a tested role, row-level security and limits. - A development project or branch. If the agent truly needs broad access, give it broad access to data that doesn’t matter. The Supabase MCP checklist covers project scoping and read-only mode.
- A small server endpoint you wrote. If one task needs elevated access, put that task behind your own function, which holds the key, checks the input and does one thing. The agent calls the function, never the key.
Also check the tables themselves. Supabase’s docs say a table in an exposed schema without RLS is readable and writable by any role with a grant on it, and ask you to enable RLS on every table in an exposed schema.2 An agent with only the publishable key still reaches a table you forgot.
If the key already leaked
Supabase’s own order of work is worth following: fix the cause of the leak first, then create a new secret key, replace the old one everywhere, and only then retire the compromised key. Deleting a secret key can’t be undone; deactivating legacy keys can be reversed if you find a client you missed. Supabase also advises a separate secret key for each backend component, so one leak doesn’t force you to rotate everything.1
For an agent, “the cause” usually means its environment: the .env it can read, the MCP config that holds the key, or the shell history it can search. Remove the key from those places before rotating, or the agent picks up the new one.
A checklist
- No secret or
service_rolekey in any file the agent can read (.env, MCP config, shell profile). - No MCP server or agent tool configured with a secret key.
- Legacy
service_roleJWT deactivated once nothing uses it. - One secret key per backend component, named for it.
- RLS on every table in an exposed schema.
- The agent’s own role has no
BYPASSRLSand doesn’t own the tables it touches (owners bypass policies unless forced).3 - Production data reached only through a narrow role or your own endpoint.
To see whether your agents can read a database URL from .env or their client config, paste the agent’s MCP config into the check page or run npx deedglass scan (how the grade works); the scan Action runs the same scan on pull requests. To try the checks in your browser without installing anything, open the playground.
Questions people ask
Is the Supabase service_role key the same as the secret key?
They do the same job. The JWT service_role key is the legacy form, and the new secret keys are short strings that Supabase recommends instead. Both authorize through the service_role Postgres role.1
Does Row Level Security protect me if an agent has the secret key?
No. service_role has BYPASSRLS, so policies never apply to a request made with the secret key alone.1
Is a publishable key safe to give an agent?
Safer, not safe by itself. It reaches only what your policies allow, so the policies and the tables without RLS decide what the agent can do.2
Written with AI assistance and checked against our claims register.