Skip to main content
A policy is the set of access rules Midplane enforces on every query: which tables an agent can read, which it can write, and everything it can’t touch. The model is identical whether you self-host or use Cloud — only where you author it differs.
Your policy is the per-database grid on a project. Each database has a Table permissions matrix and a Guardrails card; saving hot-reloads the running engine. 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 has two top-level keys, both optional: Use databases or the top-level table_access, 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 denied by the dangerous-statement guardrails even on a read_write table — so a write grant can’t be escalated into a wipe. For exactly what each level permits, how names resolve, and the full guardrail set, see table access and guardrails.

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.
Avoid default: read_write — it makes every unlisted table writable, including tables created later. midplane policy lint flags it as an error.
In the database’s Table permissions matrix, 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.
With this policy:
  • SELECT * FROM usersallowed (default: read).
  • DELETE FROM users WHERE id = 1denied (users is read-only).
  • INSERT INTO feature_flags (name) VALUES ('beta')allowed (feature_flags is read_write).
  • SELECT * FROM audit_logdenied (audit_log is deny, 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.
Bare references always resolve to 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.
With two or more databases configured, the MCP tool surface adapts — query takes a database argument and a list_databases tool appears. A single database changes none of this.
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.

Guardrails

Guardrails are categorical blocks that fire regardless of table access — a table you granted read_write still can’t be wiped or altered. Both ship on:
  • Unqualified writes — a DELETE / UPDATE with no WHERE clause (the whole-table write).
  • Schema changes (DDL)DROP, TRUNCATE, ALTER.
In the dashboard these are the Guardrails card (Block / Allow per rule); in YAML they’re the guardrails block. Switching one to Allow is opting into a risk, not a neutral default. 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

The editor’s test panel runs your policy through the engine’s real decision path and shows a pass/fail result 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.
Column masking is authored separately — in Cloud, the project’s Exposure scan. See column masking.

Examples

Complete, copy-pasteable policies for the shapes that come up most. None set a guardrails block, so the dangerous-statement guardrails stay on — 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.

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.

Author with the CLI

Scaffold, validate, lint, and test policies.