MIDPLANE_POLICY_FILE. It holds your per-table access rules, the dangerous-statement guardrails, 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, 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. Both flags 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). CREATE is not blocked. Each flag defaults true independently, so guardrails: { block_ddl: false } keeps unqualified-DML blocking on while allowing DDL.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 that the engine writes and requires: an engine that can’t enforce masking refuses the policy at boot rather than silently serving unmasked values, so a downgrade fails loud instead of leaking. 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 both guards on 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.