The short answer
On Amazon RDS, the first user you create is a member of rds_superuser. On Neon, every role created in the Console, CLI or API is a member of neon_superuser. On Supabase, the default postgres role “has admin privileges.”123 None of these is a true PostgreSQL superuser, and all of them are far more than an AI agent needs: they can create roles, and on Neon the admin role also reads and writes all data and bypasses row-level security.2
If you connect an agent with the credentials the provider gave you, the agent has that power. Create a separate role in SQL, outside the admin group, with grants for the tables it needs.
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 each admin role carries
| Provider role | Can create roles | Can create databases | Reads and writes all data | Bypasses RLS | Other |
|---|---|---|---|---|---|
RDS rds_superuser (the first user) |
Yes | Yes | Not stated as a blanket grant | Not stated | Adds extensions; grants rds_superuser, rds_replication, rds_password; sees all connections and can terminate them1 |
Neon neon_superuser (Console/CLI/API roles) |
Yes | Yes | Yes (pg_read_all_data, pg_write_all_data) |
Yes, in projects created after 15 August 2023 | Replication, pg_monitor, pg_signal_backend2 |
Supabase postgres |
Docs say “admin privileges” | service_role is the role that bypasses RLS for the API3 |
AWS’s own description of the first user is the clearest: it is “the most highly privileged database user” on the instance, created NOSUPERUSER but with CREATEDB and CREATEROLE, and AWS recommends granting rds_superuser only to users who do superuser tasks, such as DBAs.1 An agent fixing a bug is not doing superuser tasks.
The Neon row matters most for agents. A role with pg_write_all_data can write any table, and BYPASSRLS means the row-level policies you wrote don’t apply to it.2 Our post on the Supabase service_role key shows what a bypass role reaches on a table with a policy.
Membership means the agent can become the admin
A subtle point. In PostgreSQL, the attributes LOGIN, SUPERUSER, CREATEDB and CREATEROLE are never inherited through membership; a session must SET ROLE to a role that has them to use them.4 That sounds reassuring until you notice that the agent can run SET ROLE itself. SET ROLE works for any role the session user holds the SET option on, and permission checks then run as the named role.5
We tested this on a local PostgreSQL 18.6. fake_admin stands in for a provider admin group: CREATEDB, CREATEROLE, BYPASSRLS, plus pg_read_all_data and pg_write_all_data. agent_conn is a login role inside it, the way a provider puts new roles inside its admin group. The notes table has row-level security with a policy that, for this session, matches no rows.
Code A database command in SQL. The text around it says what it does.
set role agent_conn;
select current_user, count(*) as visible from notes;
set role fake_admin;
select current_user, count(*) as visible from notes;
Code What the screen or file shows, copied as is.
current_user | visible
--------------+---------
agent_conn | 0
current_user | visible
--------------+---------
fake_admin | 3
As itself, the agent’s role saw nothing: in our test, BYPASSRLS didn’t carry through membership either. One SET ROLE later it saw every row. An agent that hits “permission denied” and searches for a fix will find SET ROLE quickly. Membership in an admin group is the admin’s power, one statement away.
Check what your agent’s role can do
Run this as the role your agent connects with, or substitute its name. It shows the role’s own attributes and every role it’s a member of, directly or through other roles:
Code A database command in SQL. The text around it says what it does.
select r.rolname,
r.rolsuper, r.rolcreaterole, r.rolcreatedb, r.rolbypassrls, r.rolreplication,
array(select b.rolname from pg_roles b
where pg_has_role(r.oid, b.oid, 'member') and b.oid <> r.oid
order by 1) as member_of
from pg_roles r where r.rolname in ('agent_conn','agent_ro');
Our output:
Code What the screen or file shows, copied as is.
rolname | rolsuper | rolcreaterole | rolcreatedb | rolbypassrls | rolreplication | member_of
------------+----------+---------------+-------------+--------------+----------------+-------------------------------------------------
agent_conn | f | f | f | f | f | {fake_admin,pg_read_all_data,pg_write_all_data}
agent_ro | f | f | f | f | f | {}
Read the member_of column, not just the flags. agent_conn looks harmless in every attribute column and still reaches everything through the groups listed on the right. If your agent’s row lists rds_superuser, neon_superuser, pg_write_all_data or any admin role, it has more than it needs.
Create a role for the agent
Create the role with SQL, not the provider’s console. On Neon this is the documented way to get a role without neon_superuser: SQL-created roles aren’t granted membership in it and start with basic public schema privileges only, so you grant each permission yourself.2
Code A database command in SQL. The text around it says what it does.
create role agent_ro login password '<a long random password>';
grant usage on schema public to agent_ro;
grant select on notes to agent_ro;
That’s the shape of agent_ro above: no attributes, no memberships, one grant. From there, add only what the task needs:
- Reads only:
selecton named tables. Read-only database access lists the grants and the traps. - Some writes:
insertorupdateon named tables or columns, nevertruncate, and not table ownership (owners bypass row-level security unless it’s forced). - Limits: a
statement_timeoutand connection limit on the role. Our safe Postgres access post shows why a session can turn a role’s default timeout off, and how to close that gap.
We compare the read-only options across providers in read-only Postgres role, compared.
Provider notes
- RDS: the first user can grant
rds_superuser,rds_replicationandrds_password.1 Don’t let the agent connect as anything that can grant roles, or it can widen its own access. - Neon: creating a role in the Console is convenient and gives it admin power.2 Use SQL for every role an agent or application uses.
- Supabase: Supabase itself says the built-in roles generally aren’t for your own application, recommends a new user for every service you give access to, and warns never to give these to a third-party service unless you absolutely trust it.3 An agent is a third-party service.
A checklist
- The agent connects with a role you created in SQL, not the provider’s first user or a console-made role.
- The role check query shows no attributes and an empty or minimal
member_of. - The role owns no tables it can reach.
- Grants name tables (or columns), not schemas wholesale.
- The admin credentials live nowhere the agent can read.
To grade the role an agent connects with, run npx deedglass scan --postgres-dsn-env DATABASE_URL, which opens one read-only session as that role and deducts for a superuser or BYPASSRLS role (how the grade works), or paste an MCP config into the check page for a quick grade in the browser; the scan Action repeats it on every pull request. To try the checks in your browser without installing anything, open the playground.
Written with AI assistance and checked against our claims register.