MIDPLANE_POLICY_FILE. It holds your per-table access rules, the write rules that refuse or hold destructive statements, and per-column masks. With no file at all, every read is allowed, every write is denied, and the guardrails stay on — so the file opts specific tables into writes, masks specific columns, holds writes for review, and (rarely) relaxes a guardrail.
A policy file has two shapes:
- Single-database (the common case) — a top-level
table_accessblock. The DSN comes from theDATABASE_URLenv var. - Multi-database (
0.2.0+) — a top-leveldatabases:array, one entry per database, each with its ownurlandtable_access. Whendatabases:is present, the top-leveltable_accessand theDATABASE_URLenv var are ignored.
midplane policy validate to check a candidate file, or midplane policy lint for a security-posture review.
table_access
Per-table read/write policy. Each referenced table resolves to a level, and a query is denied if any referenced table fails its required permission. See table access for the matching semantics.
enum
default:"read"
Access level for any table not listed under
tables. One of deny, read, or read_write.read—SELECTallowed; any write denied.read_write—SELECTandINSERT/UPDATE/DELETEallowed.deny— no access at all, not evenSELECT.
map<string, enum>
default:"{}"
Per-table overrides. Keys are table names; values are
deny, read, or read_write. Keys may be schema-qualified (public.users, stripe.charges) or bare (users); the qualified key is matched first. See table access lookup order for how bare names resolve to public.guardrails
Categorical blocks on destructive operations that fire regardless of table_access (0.9.0+) — the safety net so a table you’ve granted read_write can’t be turned into a whole-table wipe or a schema change. The dangerous_statement rule reads this block. One flag per write class; the two destructive classes are on by default, even when the guardrails section (or the whole file) is omitted, so a self-host deployment is protected out of the box.
boolean
default:"true"
Deny
DELETE / UPDATE with no WHERE clause — the whole-table write. Any WHERE (even WHERE true) makes the statement qualified; this is the missing-WHERE footgun specifically, not a predicate-strength check. Detected at every DELETE/UPDATE node, including ones nested in a data-modifying CTE.boolean
default:"true"
Deny
DROP, TRUNCATE, and the whole ALTER family (including ALTER … RENAME, ALTER TYPE … ADD VALUE, ALTER ROLE). Each flag defaults independently, so guardrails: { block_ddl: false } keeps unqualified-DML blocking on while allowing DDL.boolean
default:"false"
Deny row changes (
0.17.0+) — INSERT / MERGE, and UPDATE / DELETE with a WHERE clause. Also denies writes that carry no write site at all (CREATE TABLE, CREATE TABLE AS, CREATE INDEX, SELECT … INTO), so a database that refuses row changes can’t be made to stage the same data in a new relation instead. Reads are unaffected.Unlike the other two this defaults off: refusing ordinary row changes is a deliberate lockdown, not a safety net.approvals
Hold a write the policy already permits until a human approves it (0.16.0+) — the ask value between refusing a write and allowing it. One key per write class, mirroring guardrails. Every class defaults off: guardrails are a posture you inherit, approvals are one you ask for.
A held statement never reaches the database until an approver decides. See write approvals for the flow, the reviewer queue, and the agent’s side of the contract.
boolean
default:"false"
The umbrella — hold every class. Any class key present below overrides it for that class, so
{ writes: true, row_changes: false } holds whole-table writes and schema changes but lets ordinary row changes run.boolean
default:"false"
Hold
INSERT / MERGE, and UPDATE / DELETE with a WHERE clause (0.17.0+).boolean
default:"false"
Hold
UPDATE / DELETE with no WHERE clause (0.17.0+). Only reachable when guardrails.block_unqualified_dml is off — a refusal outranks a hold.boolean
default:"false"
Hold
DROP / TRUNCATE / ALTER (0.17.0+). Only reachable when guardrails.block_ddl is off.table_access or a guardrail refused never reaches a reviewer.
requires_features
A marker the engine writes and enforces: a policy that depends on an enforcement feature names it, and an engine that doesn’t implement that feature refuses the policy at boot rather than silently not enforcing it. Unknown keys are stripped, not rejected — so without this token a downgrade would quietly turn a security control into a no-op.
You don’t write this key by hand — Cloud emits it, and
midplane policy init scaffolds it. It’s listed here because an upgrade that hits “requires enforcement feature(s) this engine does not support” is telling you the engine image is older than the policy.
column_masks
Per-column masks (0.12.0+) — a sibling of table_access / guardrails that transforms specific column values before they ever reach the agent. The block is shaped "schema.table" → (column → mask rule). A masked column must still be readable under table_access; masking shapes a value you’re already allowed to read, it never grants access. See column masking for the transform catalog and where it sits in the pipeline.
map<string, map<string, mask rule>>
default:"{}"
Outer keys are table references, resolved like
table_access: a schema-qualified key (public.users) is matched first, and a bare reference (users) resolves to public. Inner keys are column names; each value is a mask rule (below).- A preset — a bare string naming a non-parametric transform:
full-redact,null-out, orconsistent-hash. - A tagged object — a map with a
t:key naming a parametric transform plus its parameters:{ t: partial, keepStart?, keepEnd?, glyph? },{ t: generalize, granularity },{ t: pseudonymize, kind }, or{ t: noise, ratio }.
column_masks carries a requires_features: [column_masks] marker, so an engine that can’t enforce masking refuses the policy rather than silently serving unmasked values. Masking also needs a per-deployment salt — the engine refuses to boot a database that declares column_masks without MIDPLANE_MASK_SALT.
boolean
default:"false"
Apply this database’s masks by rewriting the query at the source (
0.14.0+) rather than redacting the result after execution. Overrides the engine-wide MIDPLANE_MASK_SOURCE_REWRITE default for this database; when set, the policy also carries a requires_features: [mask_source_rewrite] marker. Cloud sets it on for every masked database. In a multi-database policy it lives under each databases[] entry.databases
A top-level array (0.2.0+) that serves multiple databases through one MCP endpoint. When present, the top-level table_access and the DATABASE_URL env var are ignored (Midplane warns at boot if both are set). See multiple databases for the operational guide.
string
required
The database’s logical name, surfaced to agents as the
database argument on MCP tools and stamped on every audit row. Must match the regex ^[a-z][a-z0-9_-]{0,31}$ — lowercase, starts with a letter, dashes / underscores / digits allowed, max 32 characters. Must be unique within the array. __default__ is reserved for the legacy single-database path and may not be used.string
required
The connection string for this database. Supports
${ENV_VAR} interpolation — ${PG_PRIMARY_URL} is replaced with that env var’s value at boot. A reference to an unset or empty env var fails the boot loudly (so a typo’d reference never boots with an empty DSN). Keep the literal DSN out of the file; reference an env var instead.enum
default:"postgres"
The SQL dialect for this database. Currently only
postgres is accepted — the key exists for forward compatibility with additional dialects. Omit it for Postgres. Any other value (for example mysql) fails the boot at schema time. See the policy engine.object
Per-database
table_access, with the same default / tables shape as the top-level block above.object
Per-database
guardrails, with the same shape as the top-level block above. Omitting it keeps the two destructive guards on for that database.object
Per-database
approvals, with the same shape as the top-level block above. Omitting it holds nothing for that database.object
Per-database
column_masks, with the same "schema.table" → (column → mask rule) shape and the same boot requirements as the top-level block above. See serve multiple databases.Validation rules
Beyond the per-key types, the loader enforces these semantic rules. Each fails the boot — andmidplane policy validate — with a precise message:
Env-var interpolation failures (
${VAR} unset) only matter at connect time, so an offline validate / lint treats them as a pass. The boot path enforces them.Full examples
The inline snippets above show each block; for complete copy-paste recipes — single-DB and multi-database — see the policy cookbook.Related
Policy CLI
Scaffold, validate, lint, and dry-run a policy file.
Environment variables
Where
MIDPLANE_POLICY_FILE and DATABASE_URL fit in.