What infrastructure do you need to let AI agents read and write data safely?
Five components, working together:
- A dedicated, least-privilege role per agent: its own identity, scoped to exactly the objects and operations it needs, never a shared or admin credential.
- A read replica for exploratory and analytics traffic, so agent queries cannot contend with production writes.
- A control plane (gateway) on the connection path that classifies every operation, enforces a default-deny policy, and is the only route to the database the agent has.
- An approval channel your team already watches, where risky operations pause for a named human before they execute.
- An append-only audit log, stored outside the agent's reach, recording every operation and every decision.
Plus one component most safety checklists forget: current documentation of what the data means, so a permitted query is also a correct one. The database engine gives you only the first two. The rest lives outside the engine, and the rest is where incidents are actually prevented.
That is the conclusion. Before the architecture, here is the evidence for why the engine's own controls are not enough: measured, not asserted.
We gave an AI agent a database and measured the blast radius
Every post about agent database safety asserts the same thing: static grants are not enough, agents move too fast for a human to intervene, read-only does not stop exfiltration. We wanted numbers, so we built a realistic target and measured what a misled agent actually does before anything stops it.
The setup. PostgreSQL 18.4 on an Apple M5 with 24 GB of RAM, shared_buffers at 2 GB. A synthetic e-commerce database: 2,000,000 customers (with email, the kind of PII an attacker wants) and 20,000,000 orders spread across 50 tenants, 2.7 GB in total. Two roles: an application role with read and write, and agent_ro with SELECT only. Every destructive statement below ran inside a transaction we rolled back, so the timings reflect the real work the engine performed before the rollback undid it. In production there is no rollback.
Measured on the 20M-row database. Every destructive action finishes before the alert is read.
The results.
| What the agent did | Grant it needed | Measured result |
|---|---|---|
Exfiltrate the customer table (\copy to a local file) | SELECT only | 2,000,000 rows / 147 MB in 557 ms |
DROP TABLE orders (20M rows, 2.5 GB) | table owner / DDL | ~1 ms (size is irrelevant, it unlinks storage) |
Injected UPDATE rewriting one tenant | UPDATE | 400,213 rows in 2.6 s |
| Unbounded self-join (runaway resource use) | SELECT | 3.3 s even bounded to 1% of the table |
Same runaway, with statement_timeout = 2s | SELECT | server aborts it at exactly 2 s |
| Read every tenant's orders (no row-level security) | SELECT | 50 tenants, all 20M rows visible |
| Same query, with a one-line RLS policy | SELECT | 1 tenant, 400,213 rows (same SQL) |
Four things fall out of that table, and they are the entire argument of this guide:
- Read-only stops nothing that matters for confidentiality. The SELECT-only role dumped every customer email to a local file in about half a second. No grant was exceeded; nothing was misconfigured. Read-only is a write control, not a data-protection control.
- Destruction is instant and irreversible. A 20-million-row table is gone in a millisecond, because
DROPremoves catalog entries and unlinks files; the row count never enters the cost. The only reason our data survived is the rollback that production deployments do not have. - "Machine speed" is literal. Line up the timings against human reaction on the figure: the exfiltration, the drop, and the 400k-row update all complete before a Slack alert has finished rendering, let alone before an on-call engineer reads it. An approval gate the agent can outrun is decoration.
- The controls that do work are the ones that run without a human.
statement_timeoutaborted the runaway query at exactly two seconds. A one-line row-level-security policy turned a total tenant leak into perfect isolation on the identical query. Both are automatic, both live in the data path, and neither asks the agent's permission.
The rest of this guide is the architecture those four findings imply. The examples use Postgres because it is concrete, but the model applies to MySQL, MongoDB, and any engine your agents touch. (Reproduction details are at the end.)
Access is not a one-time grant
When a teammate asks for production database access, you do not just hand over a password. You decide what they can read, what they can change, and which actions need a second pair of eyes, and you assume that judgment is applied continuously, on every action. In July 2025, an agent on Replit showed what happens when that assumption fails with an AI in the loop: it deleted a live production database during an explicit code freeze. Our one-millisecond DROP TABLE is the same event with a stopwatch on it.
An AI agent breaks the human assumptions. It does not get tired, it does not pause to reconsider, and it will act on whatever it reads, including text that arrives from a ticket, a webpage, or a row in your own database. The question is not whether to give an agent access; for most teams running coding agents, support copilots, and analytics assistants, that decision is already made. The real question is how to grant access so that every action stays inside a boundary you defined, no matter what the agent was told to do. The honest answer is two layers working together: static controls inside the database engine, and a runtime control plane in front of it.
Layer one: static controls in the database engine
Start with what the database already gives you. These are table stakes; configure all of them before an agent connects.
- A dedicated, least-privilege role. Never let an agent reuse an application or admin account. Scope a role to exactly the schemas and operations it needs. In Postgres, the predefined
pg_read_all_datarole grants read access without write, a reasonable start for a read-only agent. (Postgres docs) - Read replicas. Point analytics and exploratory agents at a replica so their queries cannot contend with production writes or hold locks on hot tables.
- Row-level security. If an agent should only see a tenant's data, enforce it at the table with
CREATE POLICYrather than trusting the agent to add the rightWHEREclause. Our M4 result is what this buys you: same query, 50 tenants down to one. (Postgres docs) - Statement timeouts. Set
statement_timeoutso a runaway query is aborted by the server, as we saw at exactly two seconds, instead of saturating it. (Postgres docs) - Network and connection allowlists. Restrict which hosts can connect and from where.
Each control enforces something real, and each is blind to something that matters:
| Engine control | What it enforces | What it cannot see |
|---|---|---|
| Least-privilege role | Which statements this principal may run | Why the statement is being run |
| Read replica | Isolation of reads from production writes | Whether a permitted read is exfiltration |
| Row-level security | Which rows a role can touch | Volume and purpose of permitted reads |
| Statement timeout | A runtime cap per statement | Damage done by statements under the cap |
| Connection allowlist | Where connections may come from | What happens after the connection opens |
This layer is necessary. Skipping it is negligence. But the measurements show exactly where it runs out.
What static grants cannot do
Static controls operate on identity and permission: "is this role allowed to run this class of statement on this object." They cannot answer the questions that matter most once an autonomous agent is in the loop.
They cannot judge intent. A read-only role allows SELECT * FROM customers. That is the same grant whether the agent is counting active accounts or, as we measured, writing all two million email addresses to a file in 557 milliseconds. The grant sees a permitted read. It does not see purpose, volume, or sensitivity.
They cannot stop injected instructions. Prompt injection is the top entry in the OWASP Top 10 for LLM Applications, and the reason is structural: an LLM processes instructions and data on the same channel, so untrusted content (a support ticket, a scraped page, a comment field) can be interpreted as a command. (OWASP) A read-only grant survives that attack, but the moment the agent has any write ability, an injected "now delete the stale records" rides straight through a valid permission. We unpack this failure mode in read-only is not enough.
And they cannot avoid the eventual write. Read-only is a comfortable place to start but rarely where teams stay. The point of an agent is usually to fix something: apply a migration, backfill a column, drop an unused index. The instant you grant write or DDL, the static layer's protection drops to "this role is allowed to do this," which, at 2.6 seconds for 400,000 rows and one millisecond for a whole table, is exactly the protection an injected or mistaken instruction abuses.
Layer two: a runtime control plane on the connection path
The gap is everything between "the agent decided to act" and "the engine executed it." Static permissions check identity at that boundary. What you also need is a layer that inspects the actual action, in context, every time, and decides whether it proceeds.
That layer is a runtime control plane: a checkpoint in the data path, in front of the database, that intercepts each statement before it reaches the engine. Because it lives in the connection path rather than inside the model, it sees the real query the agent is about to run, not the agent's stated plan. It is engine-agnostic by design: the same plane governs a Postgres UPDATE, a MySQL ALTER TABLE, and a MongoDB deleteMany, because it reasons about the operation rather than one vendor's permission model.
Engine controls inside the database, and a runtime control plane in front of it.
The four jobs of the control plane
A control plane worth deploying does four things on every action.
- Classify. Parse the operation and label it: read or write, DDL or DML, which objects, how much data is in scope, whether it touches anything tagged sensitive. Classification turns an opaque statement into something a policy can reason about: the difference between "a SELECT" and "a SELECT of the entire PII table."
- Enforce a default-deny policy. Start from deny and allow specific, named operations. Instead of blocking the dangerous things you thought of, you permit only what you explicitly sanctioned, and everything novel is stopped by default. What that policy document should say, clause by clause, is covered in how to write a production database access policy for AI agents.
- Gate risky actions behind human approval. Schema changes, bulk deletes, anything touching financial or personal data: these pause and wait for a person. OWASP's guidance for LLM and agentic systems repeatedly lands on the same control. (OWASP) We cover how to design that gate without grinding work to a halt in human-in-the-loop database migrations.
- Record immutably. Every action, every decision (allowed, denied, approved by whom), written to an append-only log outside the agent's reach. As the audit-trail guide argues, that is the only kind the agent cannot edit.
Why enforcement must live outside the agent's context
The natural instinct is to put the rules in the prompt: "you may only run SELECT statements, never DELETE without confirmation." That is a guideline, not a guardrail.
Instruction-based guardrails fail for the same reason prompt injection works. They live in the same context window as the untrusted input, so a crafted instruction can talk the model out of them, and even without an attacker, a model can reason its way around a rule it was given. A guardrail the agent can read is a guardrail the agent can ignore.
Enforcement in the data path cannot be argued with. The control plane is not a participant in the conversation; it does not read the agent's reasoning, and the agent cannot message it. The agent emits a query, the plane classifies and checks it against policy, and an injected "ignore your previous instructions" never reaches the component making the decision. That separation is the whole point, and it is why the two controls that worked in our test, statement_timeout and RLS, worked: they enforce without consulting the agent.
How to let an AI agent run schema migrations safely
Migrations are where "the agent has write access" becomes concrete, and the blast-radius numbers make the case for a dedicated path. The safe shape has five steps:
- The agent proposes the migration as code, a reviewable diff in your migrations directory, not live DDL on a production connection.
- The change is validated on a disposable clone or branch of production, where lock behavior and duration are measured rather than guessed. Staging routinely lies about both because its data volumes do not match production; we measured how far off in the Postgres staging gap.
- A human approves the exact statement, seeing the target table, the measured impact, and the agent's stated reason, through the same gate as any other risky operation.
- Execution runs guarded, with
lock_timeoutset so the migration queues politely instead of stalling traffic behind anACCESS EXCLUSIVElock. Two teardowns show what happens without it: Railway's two migration outages and the online DDL face-off. - Everything is recorded: the proposal, the validation result, the approver, the execution window.
An agent on this path can ship schema changes faster than most human processes, precisely because every step is mechanical and auditable. An agent with standing DDL privileges on a production connection is the one-millisecond DROP TABLE waiting to happen, and no prompt makes it safe.
MCP servers change the transport, not the rules
By 2026 most agents reach databases through MCP (Model Context Protocol) servers rather than raw drivers. That standardizes the plumbing and changes nothing about the safety model. An MCP server configured with production credentials is a production connection: whatever its tools can do, the agent can do, and whatever text the agent ingests can steer it.
So the two layers apply unchanged. The MCP server's credential should be the dedicated least-privilege role from layer one, and the server should sit behind (or be) the control plane from layer two. The attack that makes this urgent, hijacking an agent through the content it reads and driving its MCP tools, is documented in agentjacking, and the argument that gateways in front of MCP are becoming table stakes is in the security gateway, 2026.
Safe access also means correct context
There is a second failure mode the blast-radius test does not capture, because it trips no control at all: the agent runs a query it is fully permitted to run, and the query is wrong. It picks total_amount when finance uses net_amount. It joins through a column that looks like a foreign key and is not. It reads a deprecated table nobody flagged as deprecated. No grant is violated, no gate fires, and the wrong number lands in a report or a customer email.
That is a context problem, not a permission problem. Agents act on what they can see, and a raw schema dump does not carry meaning: which column is the source of truth (three columns, same metric), which relationships are real, which caveats apply. It is why an agent that looks fluent against a clean test schema stumbles against a production one carrying a decade of history. The fix is a context layer: a governed, machine-readable account of what's there, what it means, and how it connects, kept current as the schema drifts. To build one by hand, we wrote a Postgres runbook.
Access controls keep the agent inside the boundary. Context keeps it right within the boundary. A production-grade setup needs both, and they are naturally one project: the same layer that documents your data is where scope, sensitivity tags, and policy attach.
A concrete Postgres walkthrough
Picture a coding agent assigned to fix a slow endpoint. It connects through the control plane, not directly to the database.
- The agent runs
EXPLAINand severalSELECTs to find the slow query. Each is classified as a read, matches an allowed rule, runs, and is logged. - It decides the fix is a new index and issues
CREATE INDEX CONCURRENTLY. The plane classifies this as DDL, which default-deny does not auto-allow. The action pauses and a request lands in your approval channel with the exact statement, the target table, and the agent's stated reason. - An engineer approves. The index builds. The approval, the approver, and the timestamp are written to the immutable log alongside the statement.
- Later, a poisoned comment in a Jira ticket tries to steer the agent into
DROP TABLE audit_log. The agent, misled, emits the statement, the same one that took one millisecond in our test. The plane classifies it as a destructive DDL on a protected object and denies it outright. The attempt is recorded. Nothing is dropped.
Swap Postgres for MySQL and the DDL is an ALTER TABLE; swap in MongoDB and the destructive call is a dropCollection. The classify, enforce, gate, record loop is identical. That engine-independence is why a control plane scales across a fleet of databases instead of needing a bespoke ruleset per engine.
Checklist: from a read-only grant to a governed path
- [ ] Dedicated least-privilege role per agent, never a shared or admin account
- [ ] Read traffic pointed at a replica where possible
- [ ] Row-level security for any tenant or sensitivity boundary
- [ ] Statement timeouts and connection allowlists configured
- [ ] All agent traffic routed through a runtime control plane, not direct connections
- [ ] Every operation classified before it reaches the engine
- [ ] Default-deny policy: explicit allows, everything else blocked
- [ ] Human approval required for DDL, bulk writes, and sensitive-data access
- [ ] Migrations proposed as code, validated on a clone, executed with lock timeouts
- [ ] MCP servers holding the scoped credential, behind the same gate as everything else
- [ ] Immutable audit log of every action and approval decision
- [ ] Enforcement outside the agent's context, so it survives prompt injection
- [ ] A documented context layer, so permitted queries are also correct queries
The first four lines are the database doing its job: the controls that survived our test. The rest is what the engine cannot do on its own.
Reproducing the measurements
The numbers above came from PostgreSQL 18.4 (default configuration except shared_buffers = 2GB) on an Apple M5, against a seeded database of 2,000,000 customers and 20,000,000 orders across 50 tenants. Exfiltration used \copy (SELECT id, email, name, created_at FROM customers) TO a local file. The DROP TABLE, UPDATE, and DELETE timings were taken inside BEGIN … ROLLBACK so the schema was untouched between runs; reported figures are warm (repeated) runs, and the one-millisecond DROP is size-independent because the statement unlinks storage rather than deleting rows. The runaway query was a self-join on customer_id; statement_timeout was set with SET statement_timeout = '2s'. Row-level security was a single CREATE POLICY … USING (tenant_id = current_setting('app.tenant_id')::smallint). Your absolute timings will differ with hardware and cache state; the orders of magnitude, and the gap between machine speed and human reaction, will not.
Where Datapace fits
Datapace is building the context layer for AI on databases: read-only, approved-scope collection by design, a living map of what's there, what it means, and how it connects, plus the operational reality around it (cost, performance, usage, lineage), and a Policy Gate over what AI may do and access, served to agents through a Context API and MCP so scope, approvals, and logging are governed rather than trusted to a prompt. We fix the base so your team can work on better things. If you are giving an agent production access, see the safe AI database access use case, compare approaches, or book a call and we will walk through it on your stack.
Sources
- OWASP Top 10 for LLM Applications: LLM01 Prompt Injection
- OWASP Top 10 for LLM Applications 2025 (PDF)
- OWASP Top 10 for Agentic Applications
- Model Context Protocol
- PostgreSQL Documentation: Predefined Roles
- PostgreSQL Documentation: Row Security Policies
- PostgreSQL Documentation: Client Connection Defaults (statement_timeout, lock_timeout)