Before you go live
Work through these before a self-hosted Midplane fronts a real database:- Secure the three secrets.
.env.self-hostholdsBETTER_AUTH_SECRET,MIDPLANE_KMS_DEV_KEY_EU, andMIDPLANE_TOKEN_PEPPER_EU_V1— auth, credential encryption, and token hashing. Keep the file out of version control, back it up, and never rotate the key or pepper once data exists (regenerating either bricks every stored credential and token). See configuration. - Back up your control-plane Postgres. One Postgres holds your projects and their encrypted DSNs, your policies, and the indexed audit log. Put it on a regular backup schedule and protect the dumps as sensitive.
- Set
MIDPLANE_MASK_SALT_MASTERbefore declaring column masks. If any project declarescolumn_masks, set the master salt first — a masked project refuses to spawn without it. Treat it as a stable secret: rotating it re-randomizes every masked value, so existing hashes and pseudonyms won’t match prior reads. - Terminate TLS at your own ingress. The control plane serves plain HTTP locally; in production route the dashboard and every project
/mcpendpoint through HTTPS at your reverse proxy or ingress. This also satisfies agents like Claude Desktop that rejecthttp://. - Give each project DSN a least-privilege database role. Point a project’s
postgres://URL at a role exactly as broad as the agent should be — Midplane’s policy and your database role must both allow a query. Two layers, not one. - Author and lint your policy. Write each project’s policy in the dashboard editor, or lint a YAML file with
midplane policy lint(it exits nonzero on any[ERROR]— wire it into CI). Keep guardrails on unless you have a specific reason. See the policy CLI and policy schema. - Wire a deny webhook so blocked queries page you instead of waiting to be found in the log — see alert on denials.
- Decide on telemetry — keep it on or disable it, see telemetry and privacy.
- Verify with the demo prompts. Connect your agent to a project, run a happy-path read and the
delete all usersdenial, and confirm both attempts land in the dashboard audit log.
Alert on denials
The audit log records every denial, but reading it is a pull. To make a denial page someone, point Midplane at a webhook and it pushes a JSONPOST the moment a query is blocked. The deny webhook is an engine feature; in self-host you set it in .env.self-host and the control plane passes it through to each project’s engine subprocess.
Set MIDPLANE_DENY_WEBHOOK to any http:// or https:// URL — the payload is generic JSON, so it works with Slack incoming webhooks, Discord, the PagerDuty Events API, or your own endpoint:
MIDPLANE_DENY_WEBHOOK_RULES, a comma-separated allowlist of rule names — table_access, multi_statement, dangerous_statement, parse_error. A denial whose rule isn’t listed is still written to the audit log; it just doesn’t fire the webhook.
POST:
2xx counts as delivered. A failure (network error, 5xx, slow endpoint) never blocks or fails the audit write: the DECIDED row always lands, so treat the audit trail as authoritative and the webhook as a best-effort alert that can miss an event if your receiver is down. Slack renders the JSON as a code block; for richer formatting, point the webhook at a small relay that translates the payload into Block Kit. For the complete field list and schema versioning, see the deny webhook reference.
Telemetry and privacy
The Midplane engine sends anonymous, aggregate usage telemetry by default — a startup ping and a daily heartbeat of counts. It never includes your SQL, your schema, or your credentials. Telemetry is an engine setting; in self-host you control it through.env.self-host, which the control plane passes through to each engine subprocess. TELEMETRY.md in the engine repo is the full source of truth.
Disable it completely with any one of these — no startup event, no heartbeats, and the install ID is never generated:
MIDPLANE_TELEMETRY=debug: each event that would be transmitted is written to stderr as one line of JSON prefixed [telemetry-debug], and no network call is made.
Two kinds of event post to https://t.midplane.ai/v1/events, both carrying a stable random install ID unrelated to your hostname, IP, database, or account:
startup— once per process: the install ID, Midplane version, bun version, OS and architecture, the transport, and whether it’s in a container or CI.heartbeat— every 24 hours: per-tool call counts (with allow/deny splits), denials grouped by rule name, coarse statement-type buckets (SELECT,INSERT, …), and Midplane-added latency percentiles (p50/p95/p99). An idle window with zero tool calls sends nothing.
agent_name / agent_version / agent_intent), and policy file contents.
Deny webhook reference
The full payload schema, every field, and version history.
Full telemetry contract
The complete field-by-field schema and install-ID lifecycle.