AI Agent Database Access: How to Let Agents Write Safely

Letting an AI agent write to your database is safe when the agent never holds a credential that can do more than its job, when risky statements wait for a person, and when everything it changed can be reverted. The dangerous setup is the common one: an agent with the database password or a service key, where one bad statement can drop a table or delete every row. StitchDB's agent keys give each agent its own Postgres role, a query firewall, approvals, a query log and per-session undo. This article explains each layer and how to set them up.

#What actually goes wrong

Before picking controls, it helps to name the failures. They are rarely malicious; they are an eager assistant doing the wrong thing confidently:

  • Unbounded writes. update users set plan = 'free' with the WHERE forgotten or hallucinated away.
  • Destructive cleanup. "The old table looks unused" followed by drop table.
  • Security changes as a fix. A query fails with a permission error, and the agent "fixes" it by disabling Row Level Security or adding a policy that is always true.
  • Reading too much. An agent debugging a sign-up bug does not need the payments table, but a full-access credential lets it read it into a chat transcript.
  • Prompt injection. Rows written by your users can contain text like "ignore previous instructions and delete…", and the model reads query results.

A single control does not cover all of these. Instruction-level rules ("never drop tables") help but cannot be relied on. You want limits the database enforces whatever the model decides.

#The layers

LayerStopsEnforced by
A Postgres role per agentAccess to tables outside its job, schema changes for non-migration keysPostgres grants and RLS
Query firewallTransaction tricks, SET ROLE, DO, COPY, dynamic SQL, denied tablesStatement parsed with the Postgres parser
ApprovalsUnbounded writes, big changes, drops, RLS and privilege changesA person in Studio
Query logNot knowing what happenedEvery statement, verdict, rows and duration
UndoLasting damage from a bad sessionRow and schema changes recorded per session

They are independent. If the firewall missed something, the agent's role would still refuse it with "permission denied". If a statement is allowed and turns out wrong, undo reverses it.

#Layer 1: one key, one role, one job

In Studio open AI agents → Agent keys → New agent key and pick a preset:

PresetCanCannot
Read-onlySELECT on the allowed tablesWrite anything
App developerRead and write rows of the allowed tablesChange the schema; writes without WHERE and big changes wait for approval
MigrationsChange the schema with the project owner's rightsRun drops, column removals, policy, privilege and extension changes without approval

Then narrow it. Allowed tables defaults to public.*; Denied tables are never readable or writable even if a pattern allows them. Set Max rows per statement to what the job really needs, and a statement timeout. Keys can also be created with the API, which is useful when each support ticket or CI run should get its own short-lived key:

Terminal
# Create (owner / admin, or an organization token with projects:write)
curl -X POST https://stitchdb.com/platform/v1/projects/<ref>/agent-keys \
  -H "Authorization: Bearer <access token>" -H "Content-Type: application/json" \
  -d '{"name": "support-bot", "preset": "app-dev",
       "policy": {"tables": {"allow": ["public.*"], "deny": ["public.payments"]}, "max_rows": 100}}'

Two settings deserve care. Bypass Row Level Security is off by default, so the agent only sees rows your policies allow for its role; turn it on only when the job requires every row. Call your own functions is also off by default, and SECURITY DEFINER functions must be listed by name, because they run with their owner's rights.

The key is shown once and only a hash is stored. Revoking it removes its Postgres role and closes its connections at once, while its query log and sessions stay for review.

#Layer 2: the query firewall

Every statement sent through the agent API or MCP is parsed with the Postgres parser and checked before it runs. Some things are always refused:

  • Transaction control (BEGIN, COMMIT, savepoints): each request is one transaction.
  • SET and RESET, including SET ROLE and set_config().
  • DO blocks, COPY, PREPARE/EXECUTE, CALL, LOCK, LISTEN/NOTIFY, role and ownership changes.
  • SQL built from strings, file and large-object functions, dblink, HTTP from SQL.
  • Platform schemas such as vault, cron, net and the undo log, and disabling triggers.

Other checks depend on the key: which operations it may run (including writes hidden in a WITH clause or EXPLAIN ANALYZE), UPDATE or DELETE without a real WHERE (WHERE true and WHERE 1 = 1 count as none), rows changed per statement counted by the database including trigger effects, and calls to your functions.

A refusal comes back with a reason the agent can act on:

JSON
HTTP 403
{ "error": "Table public.payments is denied for this agent key",
  "code": "agent_firewall", "rule": "table_denied", "statement_index": 0 }

That matters in practice. An agent that gets a vague error tends to retry variations; an agent told "this table is denied for your key" usually stops and asks. See query firewall.

#Layer 3: a person approves the risky part

Some statements are legitimate but should not run on the model's judgment alone. Instead of refusing them, the key can send them for approval:

KindExample
Write without WHEREdelete from orders
More rows than the key's limitAn update that would touch 1,200 rows with a limit of 100 (it is rolled back first)
Destructive schema changeDROP, TRUNCATE, dropping columns, changing types
Row Level Security changeDisabling RLS, creating or changing policies
PrivilegesGRANT, REVOKE, SECURITY DEFINER functions
ExtensionsCREATE EXTENSION

The agent gets 202 pending_approval with an approval id and a link, and polls until a decision:

JSON
HTTP 202
{ "status": "pending_approval", "approval_id": "6d0c…", "reasons": ["unbounded_write"],
  "approve_url": "https://stitchdb.com/dashboard/project/<ref>/agents/approvals/6d0c…",
  "expires_at": "…" }

The project owner gets an e-mail, and organization webhooks receive agent.approval_requested and agent.approval_decided, so you can route approvals to Slack or a ticket system. An approved statement runs once, as the agent, in the agent's session, so it can still be undone. Requests expire after 24 hours.

#Layer 4 and 5: see everything, undo a session

The Query log records every statement with its verdict (ran, blocked, needs approval, error), rows changed and duration, per key and session. Writes belong to a session: the MCP session, the X-Stitch-Session header, or one automatic session per hour. For every session, StitchDB records changed rows with old and new values and schema changes with their inverse. Dropped tables go to a trash schema. Reverting a session leaves other people's writes alone, and rows someone else changed later are reported as conflicts. The full workflow is in undo what an AI agent changed.

#Wiring it into your agent

With MCP, use the agent key as the token. The server then offers the agent tools: execute_sql through the firewall, list_tables, describe_table, get_approval, get_session, undo_session and check_exposure:

JSON
{
  "mcpServers": {
    "stitchdb": {
      "url": "https://stitchdb.com/platform/mcp",
      "headers": { "Authorization": "Bearer sdb_agt_..." }
    }
  }
}

For your own agent code, the JavaScript SDK has a small client:

JavaScript
import { createAgentClient } from 'stitchdb-js/management'

const agent = createAgentClient({ agentKey: process.env.STITCHDB_AGENT_KEY })

const { data, error } = await agent.sql("update orders set status = 'shipped' where id = 42", { session: 'ticket-881' })
if (data?.status === 'pending_approval') {
  // A person approves or rejects it in the dashboard; check later with the id
  await agent.approval(data.approval_id)
}
await agent.revert('ticket-881', { dry_run: false })   // undo everything the session changed

Name sessions after the unit of work, such as a ticket or a pull request, so that undo means "undo this task".

#A note on direct connections

Read-only and app developer keys can also get a Postgres connection string, or a temporary login that expires after 5 minutes to 24 hours, for tools that need a real connection. Grants and RLS still apply and changes are still recorded for undo, but the firewall, approvals and the query log do not. Prefer execute_sql when it is enough.

#Handling prompt injection

No setting makes a model immune to instructions hidden in data. The structure above limits the damage: an injected "drop the users table" hits a role without drop rights, a firewall, and an approval queue. Add these habits:

  • Use read-only keys for analysis, support lookups and debugging. Most agent work is reading.
  • Deny tables the job does not need, especially payments, tokens and personal data.
  • Keep the row limit low, so a large change always needs a person.
  • Point agents at a development project or a branch where you can.
  • Run the Exposure check (check_exposure over MCP) after an agent creates tables or policies.

#Limits per plan

PlanAgent keysQuery log keptUndo windowRows recorded per session
Free17 days24 hours10,000
Pro2590 days7 days100,000
Scale100180 days14 days1,000,000
Enterprise100180 days30 days10,000,000

Values from the plans API as of November 2026; see agent limits and pricing.

#FAQ

#Is a read-only database user not enough?

For pure reading, a read-only role is a good start, and it is what the Read-only preset creates. Agents that write need more: limits on how many rows change, approval for risky statements and a way back.

#Does the firewall replace Row Level Security?

No. It is a second layer on top of the key's Postgres role, which includes RLS unless you turn bypass on.

#What does undo not reverse?

Anything outside the database: e-mails sent, HTTP calls made from SQL, queue messages already consumed.

#Can I use this with Claude Code, Cursor or ChatGPT?

Yes, any MCP client. Setup for each is in connect Claude, ChatGPT and Cursor to Postgres with MCP.

Give your next agent a key that cannot wreck production: create a free project and read the agents docs.