- Cloud (dashboard)
- Self-host (YAML)
Your policy is the Database pane on a project: a Table access list (which tables) and a What a write may do list (which statements). Edit both, hit Save once, and the running engine hot-reloads. A new database starts read-only.
Read-only by default
The starting point is always the same: all reads allow, all writes deny. A fresh Midplane is immediately safe — an agent can explore and query, but can’t change anything until you grant a write, one table at a time. See the policy engine for how each decision is made. A policy is built from four optional keys:
A fifth key,
databases, serves more than one database from one endpoint (0.2.0+); each entry carries its own copy of the four. Use databases or the top-level keys, not both — when a databases block is present, the top-level keys and DATABASE_URL are ignored. Every key, type, and default is in the policy schema reference.
The three access levels
Every table resolves to one of three levels. A query is denied if any table it touches fails its required permission.read_write grants row writes, not whole-table destruction. A no-WHERE DELETE/UPDATE and any DROP/TRUNCATE/ALTER are refused even on a read_write table — so a write grant can’t be escalated into a wipe. For exactly what each level permits and how names resolve, see table access and guardrails; for the write classes, what a write may do.Set a default, then override per table
Pick the level for any table you don’t list, then add the exceptions:default: read— the recommended start. Reads everywhere, writes nowhere until granted.default: deny— locked down. Only the tables you list are visible. Use it when the agent should see a named allowlist and nothing else.
- Cloud (dashboard)
- Self-host (YAML)
In the database’s Table access list, set Default access to
read. Each row is a table; each column is a level. Add a row per exception and pick its level — overrides win over the default. Set feature_flags to read_write and audit_log to deny.SELECT * FROM users— allowed (default: read).DELETE FROM users WHERE id = 1— denied (usersis read-only).INSERT INTO feature_flags (name) VALUES ('beta')— allowed (feature_flagsisread_write).SELECT * FROM audit_log— denied (audit_logisdeny, no access at all).
Schema-qualified names
Table keys may be bare (feature_flags) or schema-qualified (public.users, stripe.charges). Schema-qualified keys match first, so a canonical policy still matches agent SQL that uses bare names in public.
public — Midplane pins each connection’s search path to public, pg_catalog. Tables outside public must be schema-qualified in both the policy and the agent’s SQL (for example app_data.orders: read plus FROM app_data.orders).
Serve multiple databases
One Midplane endpoint can front several databases (0.2.0+). Replace the top-level table_access with a databases array; each entry has its own connection URL and policy.
string
required
Short identifier the agent uses to pick the database — lowercase, starts with a letter, up to 32 characters.
string
required
The connection DSN. Supports
${ENV_VAR} interpolation, so DSNs and passwords stay in environment variables, not the file. An unset referenced variable fails loudly at boot.string
default:"postgres"
Only
postgres — omit it.object
The same per-table policy as above, scoped to this database.
query takes a database argument and a list_databases tool appears. A single database changes none of this.
- Cloud (dashboard)
- Self-host (YAML)
Add each database’s URL under the project; each gets its own policy grid. The project exposes one endpoint, and the agent picks a database by name.
What a write may do
Table access answers which tables. A second set of rules answers what a write may do, and fires regardless of table access — a table you grantedread_write still can’t be wiped or altered. Writes fall into three classes, each with its own independent rule (0.17.0+):
Each class takes one of three values — refuse, ask, or allow — set in the dashboard’s What a write may do list, or in YAML across the
guardrails (refuse) and approvals (ask) blocks. Ask holds the statement until a human approves it; see write approvals.
Tightening a class is always safe: refuse can never let through something ask was holding. Two deeper invariants, multi-statement queries and unparseable SQL, are always denied and have no toggle anywhere. See what Midplane blocks for the full rule set.
Test before you ship
- Cloud (dashboard)
- Self-host (YAML)
Try a statement runs one statement through the engine that enforces your policy and shows you its verdict, without touching your database. Saving then hot-reloads the running engine — no restart, no redeploy — and the edit is audited with the teammate who made it.
Examples
Complete, copy-pasteable policies for the shapes that come up most. None set aguardrails block, so whole-table writes and schema changes stay refused — add one only to relax them.
SaaS product database
Reads open by default, a couple of operational tables writable, the audit log locked out entirely.Feature flags only
Read everything, write exactly one table.Writes, but a human sees each one
Grant the write, then hold it. Row changes toorders run only once an approver says yes; everything else is unchanged.
Multiple databases
A production database plus a read-only analytics warehouse through one endpoint (0.2.0+). DSNs come from environment variables via ${ENV_VAR}, so no secrets live in the file.
Reference
Policy schema
Every key, type, and default.
Table access & guardrails
How each level behaves, plus the guardrails and rules that deny dangerous shapes.
Write approvals
Hold a write until a human approves it, and what the agent does while it waits.
Author with the CLI
Scaffold, validate, lint, and test policies.