SQLite at the edge · driven over MCP

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.

Part of Parsons · $15/mo or $150/yr · 14-day free trial, cancel any time.
parsons · databaseexecute
Clear out the test rows in habits.
the agent proposes
WITH stale AS (SELECT id FROM habits WHERE seeded = 1) DELETE FROM habits
✕refused · needs_confirm — a DELETE with no top-level WHERE affects every row. Call again with confirm: true if that is what you meant.
top-level token scanverdict · destructive
WITH stale AS (SELECT id FROM habits WHERE seeded = 1) DELETE FROM habits
The WHERE lives inside the CTE. It does not vouch for the outer delete, and the scanner knows that.
The pain, named

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.

.env
DATABASE_URL=postgresql://app:••••••@db.example:5432/prod
the context window
Pasted so the agent can "check the connection." Now it is in the transcript, forever.
a screenshot
Someone shares the terminal to ask a question. The string is in the image.
a client bundle
One misprefixed env var and it ships to the browser.
a support ticket
The agent reads inbound text. Inbound text can contain instructions.
a log line
An error handler prints the config it failed to connect with.
Read-only mode is safe and useless. Write mode is useful and terrifying. The industry's answer to both is a flag you can forget to set.
The turn

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.

How it usually works
agentone credential · can do anythingdb
↻set a read-only flag on the servera thing you can forget
↻branch, verify, then promotea thing the agent must choose
↻read the defence-in-depth blog posta thing nobody reads twice
One pipe, full authority, then a procedure bolted on the side. Procedures are things a human remembers to follow — and the credential is still sitting there when they don't.
How this works
whoami
docs
create_database
list_databases
get_database
query read only
execute confirm
migrate confirm
migration_history
export
db_usage
delete_database confirm
Twelve tools, twelve contracts. Grant your assistant the read tools and it physically has no route to a write. That is a boundary, not a setting.
Reads and writes

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.

query · execute · confirm
toolquery200 · rows
{
  "rows": [
    { "id": 1, "name": "Read 20 pages", "streak": 9 },
    { "id": 2, "name": "Walk the dog",  "streak": 3 }
  ],
  "rowCount": 2,
  "truncated": false
}
read access and write access are not a flag — they are two tools your AI can be granted separately
The craft bit

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.

SELECT * FROM habits WHERE user_id = ?1
A plain read. Runs on query, paged at 1,000 rows with a truncated flag.
safe
SELECT '-- DROP TABLE users' AS note
Not flagged. The scanner skips string literals, so scary-looking data is still just data.
safe
UPDATE habits SET streak = 0
Destructive: an UPDATE with no top-level WHERE touches every row.
confirm
WITH stale AS (SELECT id FROM habits) DELETE FROM habits
Destructive. The WITH resolves to DELETE, and a WHERE buried in a CTE body does not vouch for the outer statement.
confirm

Destructive means DROP, TRUNCATE, ALTER, or a DELETE/UPDATE with no top-level WHERE.

Confirm, not prevent

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.

beat 1 · propose
The agent writes the statement it thinks you want.
execute · DELETE FROM habits
beat 2 · refused
Not a warning and not a silent no-op. A structured refusal carrying the remedy.
409 · needs_confirm
beat 3 · you decide
"Yes, wipe them." A deliberate act by a human, not an autocomplete.
confirm: true
beat 4 · applied
It runs, and the audit row is written before the call returns.
rowsAffected: 3
at (utc)actortoolstatementresult
2026-08-19T21:14:07Zu_1 · mcpexecuteDELETE FROM habitsrefused · needs_confirm
2026-08-19T21:14:41Zu_1 · mcpexecuteDELETE FROM habits · confirmok · 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.)

Migrations that remember

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.

migrate · migration_history
applied · 2026-07-12
CREATE TABLE habits (id INTEGER PRIMARY KEY, user_id TEXT, name TEXT)
Initial habits schema. user_id is the auth subject, not an email — emails change.
applied · 2026-07-19
ALTER TABLE habits ADD COLUMN streak INTEGER DEFAULT 0
Streak is denormalised on purpose — recomputing it from the log was the slowest query on the weekly view.
applied · 2026-07-26
CREATE INDEX idx_habits_user ON habits(user_id)
The user list was doing a full scan at ~40k rows.
three weeks later · a different chat
Add a weekly target per habit.
Checked migration_history first — the July 19 note says streak is denormalised deliberately, so I won't recompute it. Proposing ALTER TABLE habits ADD COLUMN weekly_target INTEGER.
Zero setup, literally

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.

Deleting the app does not delete the data. The binding is released; the database stays yours to browse, export or delete on your own schedule.

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.

01create a database account
02create a project, pick a region
03copy the connection string
04paste it into .env
05add it to your deploy secrets
06install and configure a driver
07rotate it when it leaks
env.DB
The one line that survives. Bound by Deploy, scoped to your project, no credential in it.
The console

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.

app-data db_9f2c41 · bound to habits-web · read replication: auto no destructive control exists on this surface
habitshabit_logusersmigrations_log
iduser_idnamestreakcreated_at
1u_1Read 20 pages92026-07-12T08:31:02Z
2u_1Walk the dog32026-07-12T08:31:02Z
3u_2Stretch02026-07-14T19:05:44Z
read-only sql · SELECT / PRAGMA / EXPLAIN
SELECT user_id, COUNT(*) FROM habits GROUP BY user_id
Run
↑There is no drop button in that toolbar, and no hidden menu holding one. The console API exposes list, detail, usage, migrations, row browse and a read-only query — there is no route from this surface to execute, migrate, create_database or delete_database at all. Mutations live on the AI side, where the confirm gate is.
Three ways in, one database

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.

env.DB
Native binding from your deployed worker. No HTTP hop, no round-trip, no key.
costs: nothing to configure
MCP tools
Your AI, or the console's read surface. Scoped to the project pinned on the connection — never passed as an argument.
costs: nothing · tool calls are uncapped and free
/v1/database/*
A plain HTTP path for a backend that isn't the bound worker. Uses a sk_live_… project key — that is a credential, and we would rather say so than pretend.
burst 120 · 1,200 per minute sustained
db_9f2c41 · app-data
One SQLite database at the edge, with global read replication on by default.
project acme-storefrontget_database db_9f2c41200 ok
project client-workget_database db_9f2c41404 not_found

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.

The numbers, and where they're measured

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.

db_usage · get_database
Databases25 per account
count is not a meter — a database costs Cloudflare nothing25 included
Storagegauge · pooled across every project
acme-storefront · client-work · personal — one pool1 GB · then $1.15 / GB·mo
Rows readcounter · resets on the 1st, UTC
100,000,000 / monththen $0.002 / million
Rows writtencounter · resets on the 1st, UTC
1,000,000 / monththen $1.50 / million

D1 additionally caps any single database at 10 GB, whatever your pool.

Metering is hour-bucketed and idempotent, so a double-counted month is structurally impossible. Sizes are reconciled by a sweep every 15 minutes, not estimated.
The safety cap, high above the allowance, is soft, and we say so: there is no way to freeze a write mid-transaction on D1, so going over refuses new provisioning loudly and shows up in db_usage rather than pretending to stop a write that is already in flight.
Drive it from the AI you already use

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.

one connection · minted for you at sign-inGET YOURS
Works in Claude, ChatGPT, Cursor — anything that speaks MCP.
I need habits per user with a streak count.
Created habits with a user_id index and a streak column, logged the migration, seeded three rows and read them back — two users, three habits.
migrateexecutequery
How close am I to the storage cap?
4 databases across 3 projects, 61 MB of your 1 GB pool. Measured 6 minutes ago.
db_usage
whoamidocscreate_databaselist_databasesget_databasedelete_databasequeryexecutemigratemigration_historyexportdb_usage
Straight about the edges

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.

SQLite, not Postgres
Real SQL, real indexes, real joins — but SQLite's dialect and no extensions. No pgvector, no PostGIS, no stored procedures.
use instead
Neon or Supabase. Both are genuinely good at Postgres, and if you need the extension ecosystem you need Postgres.
No branching, no backups, no point-in-time restore
There is no "try the migration on a copy first" workflow here. The confirm gate is a different kind of safety and it is not a substitute.
use instead
If testing a migration on a copy before it lands is core to how you work, price the Postgres hosts built around branching — Neon is the usual starting point. We do not have it.
Export is schema plus 1,000 rows per table
export returns a SQL dump — full schema DDL, and up to 1,000 rows per table. A full-size dump handed off through Storage is designed but not shipped.
plan around it
Do not treat today's export as your complete exit plan. If a byte-exact dump is a requirement for you right now, this is a real gap.
1 GB included is small
Pooled across every project. Past it you are on prepaid credits at $1.15/GB·mo. If your data is genuinely big, we are not your database yet and no amount of copy changes it.
use instead
If raw storage size is your deciding factor, compare a dedicated SQLite host such as Turso before you buy here. We would rather you check the number than churn over it.
A 50-statement batch is not a transaction
Statements apply in order with no rollback, so a failure part-way leaves the earlier ones applied — the error tells you how many landed. Validation happens up front, so bad SQL never half-runs.
also true
Per-end-user isolation is your job. The database is isolated per project, not per user; scope rows by the login token's subject in your own queries.
Projects are walls, not line items

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.

personal
2 databases
cap $10/mo
acme-storefront
3 databases
cap $60/mo
client-work
4 databases
cap $25/mo
the-side-thing
1 database
cap $2/mo
staging
2 databases
cap $0/mo
$15/month, total.
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.

Pricing

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.

Base
$15 / month
or $150 / year — two months free
  • 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
Start your trial
What the same $15 also coversBase allowances, other products
Deploy1,000,000 requests · 1 GB app storage
Storage5 GB of objects
Authmonthly active users — uncapped
MCP Hubtool calls — uncapped, free
Email200 sent / day
Scheduler20,000 fires
Tracker10,000 tasks
CRM50,000 contacts
Notifications200 email messages
Radar100,000 checks
Money2 bank connections
Phone120 voice minutes
one account · unlimited projects$15.00 / month · $150.00 / year
01 · a database alone is cheaper elsewhere
We will not pretend otherwise: Turso's Developer tier is $4.99 and PlanetScale publishes $5 a month for Postgres Standard. If a database is the only thing you want, buy one of those. $15 is not the price of this database — it is the price of the account, and the account is this plus Deploy, Storage, Auth, Email, Scheduler, Tracker, CRM and the rest, on one login.
02 · nothing multiplies it
No per-seat line, no per-project fee, no per-workspace charge. Add a teammate, a fifth project, a twenty-fifth database — the number does not move. On Supabase, every project past the first adds its own compute on top of the plan; here projects are the free part.
03 · consumption stays separate
The things that genuinely cost money — AI tokens, voice minutes, phone numbers — are prepaid, capped per project and billed from a wallet you top up. The subscription is access, not a loss-leader hiding a metered bill, and no meter runs while you sleep.

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.

Questions people actually ask

The awkward ones, answered flatly.

Can my agent still drop a table if it decides to?
Only if you answer yes. DROP is classed destructive, so the first call comes back refused with the reason and the remedy; it runs on a second call carrying confirm: true. If you never grant the execute and migrate tools to that assistant in the first place, there is no write route at all. See the classifier.
What if the agent just re-sends with confirm: true on its own?
Then it has done a second, explicit, separately audited thing — and both calls are in the audit log with the actor and the statement. The gate is designed to make destruction a deliberate act rather than an autocomplete, and the strongest version of it is not granting the write tools to an unattended assistant. See the audit row.
Where does the connection string live?
There isn't one on the two common paths. Your deployed app gets a native env.DB binding; your AI gets named tools on a connection minted at sign-in. The one exception is the /v1/database/* HTTP path, which uses a sk_live_… project key — that is a credential, and we would rather name it than claim we have none. See the three doors.
What happens if I go past 1 GB?
The extra draws from prepaid credits at $1.15/GB·mo — nothing is refused at the allowance line. Far above it sits a safety cap: hit that and new provisioning is refused loudly, with the number and an ask-us-to-raise-it path in the billing console. Writes to an existing database are not frozen mid-flight — D1 has no mechanism for that, so we tell you the ceiling is soft rather than pretending otherwise. See the meters.
Can I get my data out?
Partly, today. export returns a SQL dump with the full schema and up to 1,000 rows per table. A full-size dump handed off through Storage is designed and not shipped. If a byte-exact export is a hard requirement, that is a real gap and you should know it before you sign up. See the honest list.
Is this Postgres?
No. It is SQLite on Cloudflare D1 — real SQL, real indexes, real joins, global read replication on by default, but SQLite's dialect and no extensions. If you need pgvector or PostGIS, use Neon or Supabase; we would rather you succeed than sign up.
Is there a yearly price?
Yes, and it is the plain kind. $150 a year against $180 paid by the month. That is exactly two months free — 16.7%, and you can check the arithmetic. You pick the interval in checkout, the 14-day trial is the same either way, and there is no yearly-only feature set. See the plan.
How many projects can I make?
As many as you like, at no extra cost. Each has its own databases, members and spend cap. The 25-database count and the storage pool are counted account-wide across all of them, which is why projects are free — they are organisation, not capacity. See the grid.

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.

$15/mo or $150/yr · 14-day free trial, cancel any time.