Your AI gets a database. Not the keys to one.
The usual answer is to hand your agent a connection string and then try to constrain what it does with it. We never issue one. Your AI holds twelve named tools — and reading and writing are two different tools.
The credential is the whole problem.
The incidents all start the same way: a string that can do anything ends up somewhere it can be read. Not because anyone was careless — because a connection string has to travel to be useful, and an agent's job is to move text around.
So we don't issue one.
There is no string to leak because there is no string. Your AI is granted a set of named tools with fixed contracts — and the tool that can delete a table is not the tool it uses to read one.
Two different tools.
Not a mode you set.
query takes exactly one SELECT, PRAGMA or EXPLAIN. Hand it an INSERT and it does not run it in a sandbox or warn you — it rejects it, because there is no write path behind that tool at all. Multi-statement SQL is rejected for the same reason.
execute is where writes live: one statement, or up to 50 in a batch. Anything destructive comes back refused with the reason and the remedy, and runs on the second call with confirm: true.
{
"rows": [
{ "id": 1, "name": "Read 20 pages", "streak": 9 },
{ "id": 2, "name": "Walk the dog", "streak": 3 }
],
"rowCount": 2,
"truncated": false
}{
"error": "bad_request",
"message": "query is read-only (SELECT / PRAGMA /
EXPLAIN only) — use execute for
writes, migrate for DDL."
}{
"error": "bad_request",
"message": "query accepts a single statement."
}It reads SQL like a parser, not like a regex.
"Does the statement start with DROP" is trivially defeated. The classifier scans top-level tokens only — skipping string literals, quoted identifiers, comments and anything inside parentheses — and resolves a WITH to the verb it actually performs. Statements are capped at 100,000 characters.
Destructive means DROP, TRUNCATE, ALTER, or a DELETE/UPDATE with no top-level WHERE.
A brake. Not a wall.
Nothing is forbidden — everything destructive is asked about, once, with the exact reason and the exact remedy in the refusal. Your AI keeps full speed on everything reversible and stops dead in front of everything that isn't.
| at (utc) | actor | tool | statement | result |
|---|---|---|---|---|
| 2026-08-19T21:14:07Z | u_1 · mcp | execute | DELETE FROM habits | refused · needs_confirm |
| 2026-08-19T21:14:41Z | u_1 · mcp | execute | DELETE FROM habits · confirm | ok · 3 rows |
Audit rows are awaited inline, never fire-and-forget — the record of a delete cannot vanish when the worker tears down. And a 50-statement batch is validated in full before any statement runs, so malformed SQL costs nothing. (A batch is still not a transaction — see the honest list below.)
The next session reads
what this one did.
Schema changes go through migrate, which applies the DDL and logs it with a free-text note of up to 4,000 characters. migration_history reads it back verbatim, newest first.
That is the answer to the most boring failure in agent-assisted development: the model knows the schema it wrote on Tuesday and has no idea what it did on Monday, so it generates SQL against a column that no longer exists. Leave real reasoning behind — not "migration 007", but why.
Deploy made one
before you asked.
Ship an app on Parsons and a database is provisioned for it at deploy time and bound into your worker as env.DB — a native binding, no HTTP hop, no key, nothing to paste. You never call create_database unless you want a second one.
One database, two ids: any databaseId argument accepts either this service's db_… handle or the raw Cloudflare D1 uuid that Deploy reports as resources.d1Id. Neither form 404s.
The console observes. The AI writes.
Signed in, you get a read surface: every database in the project, with the account-wide quota sitting above it — a table browser, paged rows, the migration timeline, live usage, and an ad-hoc SQL runner that is read-only by construction, not by convention.
| id | user_id | name | streak | created_at |
|---|---|---|---|---|
| 1 | u_1 | Read 20 pages | 9 | 2026-07-12T08:31:02Z |
| 2 | u_1 | Walk the dog | 3 | 2026-07-12T08:31:02Z |
| 3 | u_2 | Stretch | 0 | 2026-07-14T19:05:44Z |
Same rows. Different doors.
The conversational surface, your running app and a plain HTTP backend are not three copies of anything. They are the same database, reached the way each caller should reach it.
Every databaseId resolves through a project-scoped registry, so another project's database is not refused with a permissions error — it does not resolve. On top of that, every tool call re-verifies project ownership against Auth, not just the router's scope check.
Flat, account-wide, and visible.
None of these turn into a surprise bill. Go past an allowance and the extra simply draws from prepaid credits at roughly wholesale-plus-half — building stays free inside the allowances, and the bill starts when real usage does. The count of databases never meters, storage is a single pooled allowance across every project you own, and rows are a monthly budget that resets on the first of the UTC month.
All of it is readable at any time with db_usage and get_database, which returns your quota bar alongside the live table list — so an AI can orient itself in one call.
D1 additionally caps any single database at 10 GB, whatever your pool.
Schema design, as a conversation.
Describe what you are storing. It designs the tables, applies the migration with a note, seeds a few rows and reads them back to prove it worked — then does the same debugging pass a week later when something is off.
The docs tool serves the same bytes as the public /docs endpoints, so your AI can read this product's own documentation mid-conversation instead of guessing at the API. And whoami tells it exactly which project and scopes it is holding.
Five reasons to pick something else.
A comparison table that only flatters us would read as a lie. Here is where we lose, and who we would send you to.
Unlimited projects. Free.
A project is a hard wall: its own databases, its own members, its own spend cap, its own MCP connection. Make one per business, one for the side thing, one for personal. The price does not move.
cap $10/mo
cap $60/mo
cap $25/mo
cap $2/mo
cap $0/mo
Not per project. Not per seat.
Here the 25-database count and the storage pool are account-wide, so projects cost you organisation, not money. That is not universal — on Supabase, every project past the first adds its own compute cost on top of the plan. Nor is it unique to us: Neon includes 100 projects on its free plan. None of the hosts named on this page charges per seat. Competitor plans checked against their published pricing pages, August 2026.
One plan. It covers everything.
Database is not sold on its own. The subscription is the account, and the account is every Parsons product, across unlimited projects. Pay by the month, or pay for the year and two of them are free. Go past an included allowance and the extra draws from prepaid credits at roughly wholesale-plus-half — the bill starts when real usage does.
- 25 databases per account — count is never the meter
- 1 GB of storage, pooled across every project
- 100,000,000 rows read / month
- 1,000,000 rows written / month
- Global read replication, on by default
- Every other Parsons product, one login
14-day free trial on your first subscription · monthly or yearly, your pick at checkout · cancel any time. There is no free tier, no per-seat line and no enterprise tier — the trial is the only free part, and after it you pay.
The awkward ones, answered flatly.
Can my agent still drop a table if it decides to?
What if the agent just re-sends with confirm: true on its own?
Where does the connection string live?
What happens if I go past 1 GB?
Can I get my data out?
Is this Postgres?
Is there a yearly price?
How many projects can I make?
Let it write the schema. It still has to ask.
A database your AI can drive at full speed, with the one brake that matters left on.