The recipe below builds three task roles and two agent logins. The human operator holds the DDL role; the agent logins are members of narrower roles and do not inherit by default.
-- 1. Shut the default door
REVOKE ALL ON SCHEMA public FROM PUBLIC;
REVOKE CREATE ON SCHEMA app FROM PUBLIC;
-- 2. Task roles carry verbs, not people
CREATE ROLE agent_read NOLOGIN;
GRANT USAGE ON SCHEMA app TO agent_read;
GRANT SELECT ON app.orders, app.customers, app.tickets TO agent_read;
CREATE ROLE agent_write NOLOGIN;
GRANT agent_read TO agent_write;
GRANT INSERT, UPDATE ON app.tickets TO agent_write;
GRANT USAGE ON SEQUENCE app.tickets_id_seq TO agent_write;
-- 3. DDL role, held by humans only
CREATE ROLE ddl_owner NOLOGIN;
GRANT CREATE ON SCHEMA app TO ddl_owner;
-- ownership of existing tables stays with the human admin login
-- 4. A login per agent, no inheritance surprises
CREATE ROLE agent_triage LOGIN PASSWORD :'pw' NOINHERIT;
GRANT agent_write TO agent_triage;
ALTER ROLE agent_triage SET statement_timeout = '5s';
ALTER ROLE agent_triage SET idle_in_transaction_session_timeout = '60s';
CREATE ROLE agent_answers LOGIN PASSWORD :'pw2' NOINHERIT;
GRANT agent_read TO agent_answers;
ALTER ROLE agent_answers SET statement_timeout = '15s';
ALTER ROLE agent_answers SET default_transaction_read_only = on;
Four decisions in that script carry most of the weight.
First, the REVOKE ... FROM PUBLIC lines. Because every role belongs to PUBLIC per Prisma's privilege guide, leaving PUBLIC with schema usage or create rights hands every future agent login a floor of access you did not choose.
Second, sequence privileges. A role that can INSERT into a table with a serial or identity column also needs USAGE on the backing sequence, which is granted with the ON SEQUENCE form of GRANT per the PostgreSQL GRANT documentation. Miss this and the agent's writes fail with a confusing permission error on an object it never named.
Third, NOINHERIT. Without inheritance, membership in a role does not automatically confer its privileges, and the session has to switch into that role to use them, which is the attribute the Prisma guide contrasts with the automatic inheritance path per Prisma's privilege guide. For an agent this is a feature: you can grant a role for later use without exposing it in the default session.
Fourth, default_transaction_read_only. This makes the analytics login refuse writes at the session level, which catches the case where someone later grants a write verb to agent_read by mistake. Combine it with the explicit SELECT-only grant rather than relying on either one alone.
The read-only user deserves its own paragraph. Retrieval and question-answering tasks need to scan tables an agent will never write, and they benefit from a login that physically cannot write. Build it as a role with SELECT only, membership in it for exactly the logins that do retrieval, and default_transaction_read_only = on. If the retrieval service uses a connection pool, check that the pool does not reset session settings per connection in a way that drops your defaults.