> ## Documentation Index
> Fetch the complete documentation index at: https://midplane.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Policy file schema

> Every key in the MIDPLANE_POLICY_FILE YAML: table_access, guardrails, column_masks, and the multi-database databases[] block.

The policy file is the YAML you mount via [`MIDPLANE_POLICY_FILE`](/docs/reference/environment-variables). 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_access` block. The DSN comes from the `DATABASE_URL` env var.
* **Multi-database** (`0.2.0+`) — a top-level `databases:` array, one entry per database, each with its own `url` and `table_access`. When `databases:` is present, the top-level `table_access` and the `DATABASE_URL` env var are ignored.

The file is validated against the exact same schema the server boots with. Run [`midplane policy validate`](/docs/reference/cli) to check a candidate file, or [`midplane policy lint`](/docs/reference/cli) for a security-posture review.

<Warning>
  Unknown keys are silently ignored, not rejected. A misspelled key like `table_acess` is dropped and the default applies, so `validate` won't catch the typo — and a silently widened access slips through. Double-check spelling.
</Warning>

## `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](/docs/concepts/table-access) for the matching semantics.

<ParamField path="table_access.default" type="enum" default="read">
  Access level for any table not listed under `tables`. One of `deny`, `read`, or `read_write`.

  * `read` — `SELECT` allowed; any write denied.
  * `read_write` — `SELECT` and `INSERT` / `UPDATE` / `DELETE` allowed.
  * `deny` — no access at all, not even `SELECT`.
</ParamField>

<ParamField path="table_access.tables" type="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](/docs/concepts/table-access) for how bare names resolve to `public`.
</ParamField>

```yaml theme={null}
table_access:
  default: read           # default for tables not listed below
  tables:
    users:            read
    posts:            read
    audit_log:        deny
    feature_flags:    read_write
    "stripe.charges": read    # schema-qualified key, matched first
```

## `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.

<ParamField path="guardrails.block_unqualified_dml" type="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.
</ParamField>

<ParamField path="guardrails.block_ddl" type="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.
</ParamField>

```yaml theme={null}
guardrails:
  block_unqualified_dml: true   # deny DELETE/UPDATE with no WHERE
  block_ddl: true               # deny DROP/TRUNCATE/ALTER
```

<Warning>
  Disabling a guardrail widens what a `read_write` table can do. `midplane policy lint` warns on an explicit opt-out (`block_ddl: false` / `block_unqualified_dml: false`).
</Warning>

## `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](/docs/concepts/masking) for the transform catalog and where it sits in the pipeline.

<ParamField path="column_masks" type="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).
</ParamField>

A **mask rule** is one of two shapes:

* A **preset** — a bare string naming a non-parametric transform: `full-redact`, `null-out`, or `consistent-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 }`.

See [column masking](/docs/concepts/masking) for what each transform does, its parameter defaults, and which types it applies to.

```yaml theme={null}
table_access:
  default: deny
  tables:
    users: read              # a masked column must be readable
column_masks:
  public.users:
    email: full-redact       # preset
    user_id: consistent-hash # preset
    ssn:
      t: partial             # tagged object
      keepEnd: 4
    dob:
      t: generalize
      granularity: year
    balance:
      t: noise
      ratio: 0.1
```

A policy with `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`](/docs/reference/environment-variables).

<ParamField path="mask_source_rewrite" type="boolean" default="false">
  Apply this database's masks by [rewriting the query at the source](/docs/concepts/masking) (`0.14.0+`) rather than redacting the result after execution. Overrides the engine-wide [`MIDPLANE_MASK_SOURCE_REWRITE`](/docs/reference/environment-variables) 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.
</ParamField>

## `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](/docs/policies/overview#serve-multiple-databases) for the operational guide.

<ParamField path="databases[].name" type="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.
</ParamField>

<ParamField path="databases[].url" type="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.
</ParamField>

<ParamField path="databases[].dialect" type="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](/docs/how-it-works).
</ParamField>

<ParamField path="databases[].table_access" type="object">
  Per-database `table_access`, with the same `default` / `tables` shape as the top-level block above.
</ParamField>

<ParamField path="databases[].guardrails" type="object">
  Per-database `guardrails`, with the same shape as the top-level block above. Omitting it keeps both guards on for that database.
</ParamField>

<ParamField path="databases[].column_masks" type="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](/docs/policies/overview#serve-multiple-databases).
</ParamField>

## Validation rules

Beyond the per-key types, the loader enforces these semantic rules. Each fails the boot — and [`midplane policy validate`](/docs/reference/cli) — with a precise message:

| Rule                | What's rejected                                                         |
| ------------------- | ----------------------------------------------------------------------- |
| Database name regex | A `databases[].name` that doesn't match `^[a-z][a-z0-9_-]{0,31}$`.      |
| Reserved name       | A `databases[].name` of `__default__`.                                  |
| Duplicate names     | Two `databases[]` entries with the same `name`.                         |
| Unset `${VAR}`      | A `databases[].url` that references an env var which is unset or empty. |
| Unknown dialect     | A `databases[].dialect` that isn't `postgres`.                          |

<Note>
  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.
</Note>

## Full examples

The inline snippets above show each block; for complete copy-paste recipes — single-DB and multi-database — see the [policy cookbook](/docs/policies/overview#examples).

## Related

<Columns cols={2}>
  <Card title="Policy CLI" icon="terminal-square" href="/docs/reference/cli">
    Scaffold, validate, lint, and dry-run a policy file.
  </Card>

  <Card title="Environment variables" icon="settings-2" href="/docs/reference/environment-variables">
    Where `MIDPLANE_POLICY_FILE` and `DATABASE_URL` fit in.
  </Card>
</Columns>
