Skip to main content
The fastest way to diagnose a Midplane issue is to read the denial reason or the boot log — both name what’s wrong and usually what to change. This page collects the issues people hit most, with the fix for each.

Denied queries

A denial is usually correct: Midplane is doing its job. But if a query you expected to succeed was denied, the reason string names the rule and the table. Match it to one of these.
The likely cause: the table is set to deny. A table marked deny allows neither reads nor writes. Check your policy — if the table’s access is deny (or your default is deny and the table isn’t listed), reads are blocked. Set it to read (or read_write) to allow SELECTs. See table access.
Writes deny by default. To allow a write, mark the table read_write in your policy:
In the project’s Database pane, add a per-table override under Table access and set it to read_write, then Save. Cloud hot-reloads the change.
Note that read_write grants ordinary DML only. Side-effecting statements — COPY, LOCK, CALL, NOTIFY, SET — deny regardless of YAML, because the policy can’t grant a per-table target for them.
Not a denial — the write is held for a human and hasn’t run. Approve or deny it from Approvals in the dashboard; the agent then re-runs the identical statement to collect the result.Two things trip people up. The grant is bound to the exact sql and intent, so an agent that paraphrases its retry opens a second request instead of collecting the first — the held result hands both strings back for exactly this reason. And a request nobody answers expires after 30 minutes, after which the agent has to ask again.
The engine can’t reach the approval service, so it’s refusing to run held writes rather than running them unreviewed. Nothing was denied and nothing is lost — the writes succeed once the gate answers.On self-host the usual cause is the callback origin: localhost inside a container is the container, so an engine told to call http://localhost:3000 gets connection-refused. Set MIDPLANE_APPROVAL_CALLBACK_ORIGIN to something the engine can actually reach — http://host.docker.internal:3000 on Docker Desktop.

Agent connection problems

The client’s OAuth session lapsed and it’s asking you to sign in again — some clients (Cursor in particular) render that as a connected server with an empty tool list rather than a prompt. Re-run the client’s sign-in for the Midplane server; the consent screen pre-selects your current grant, so nothing about the scope changes.A connected agent stays connected as long as it’s used: the session refreshes itself on every call. The clock only runs while the client is quiet, and it takes 90 days of no activity to lapse. An agent you revoked also stops working immediately — check the project’s Agents pane before re-authorizing.
The most common cause is a missing transport flag. Claude Code defaults to stdio and will treat your URL as a launcher command, failing silently. Add --transport http:
Verify it registered with claude mcp list. See Claude Code setup.
Claude Desktop’s Custom Connectors UI enforces HTTPS and rejects http://localhost (and won’t accept a self-signed cert against localhost). For a local self-host instance, use the mcp-remote shim in the config file instead:
This requires npx (Node.js) on your PATH. Restart Claude Desktop after editing the file. Hosted Midplane (an HTTPS endpoint) uses Custom Connectors normally. See Claude Desktop setup.

Connecting to your Postgres

Your database presents a self-signed or private-CA TLS certificate and its DSN uses sslmode=require (or prefer). Since engine 0.15.0 Midplane follows libpq’s TLS semantics, where require encrypts without verifying the certificate — so a self-signed cert connects. On an older engine, require was upgraded to full verification and rejected the cert with a bare self signed certificate the moment an agent ran its first query.
  • Encryption is enough (a self-signed or private cert) — sslmode=require connects on 0.15.0+; upgrade the engine if you’re behind.
  • You want the cert verified — add sslrootcert=/path/to/ca.crt to the DSN and Midplane verifies against that CA (like verify-ca). verify-ca / verify-full are unchanged.
  • Encryption isn’t needed (a private, trusted network) — sslmode=disable turns TLS off.
A DSN with no sslmode behaves exactly as before. On Cloud the managed engine already runs 0.15.0+, so this applies to whichever database you connect.

Boot failures

The standalone engine needs Node 22.16+ or 24+ — its audit log uses the node:sqlite builtin — or Bun 1.3+. On anything older the binary refuses to start and names the requirement, rather than failing partway through with a stack trace from whichever dependency reached a newer builtin first.Check with node --version. If your MCP client launches with a different Node than your shell (a common cause when the client was installed by a version manager), give the config an absolute path to a supported npx.
Midplane interpolates ${ENV_VAR} references in databases[].url so DSNs stay out of your YAML. If a referenced variable isn’t set in the environment, the URL can’t resolve and the server fails to boot.Check that every variable named in your policy file is present in your --env-file (or compose environment). For example, a policy with url: ${PROD_DATABASE_URL} needs PROD_DATABASE_URL defined.You can structurally validate a policy offline without the variables set — midplane policy validate <file> doesn’t fail on an unset ${VAR}, so it checks the YAML shape independent of the environment. The variable only has to resolve at server boot.
When in doubt, read the audit log. midplane audit tail streams every attempt and decision, including the rule that denied — so you can see exactly what your agent sent and why it was blocked. See the self-host audit log.

FAQ

Short, grounded answers to the questions that come up most.
Yes — and we don’t claim otherwise. Midplane logs every query as the audit record, and results pass through its memory on the way back to your agent (never persisted). On self-host none of this leaves your environment; on Cloud, operators with database access can read audit data until retention expires. See what we claim, and what we don’t.
No. Midplane is a second layer. It enforces policy on top of your database’s own permissions — it doesn’t replace role-based access control. If the connection string belongs to a privileged role, Midplane operates on top of those permissions. Give the agent a least-privilege database role as well. See the threat model.
No, not by default. read_write grants row writes; whole-table destruction is a separate net. The dangerous_statement guardrails — on by default since 0.9.0 — deny a DELETE/UPDATE with no WHERE clause and any DROP/TRUNCATE/ALTER, regardless of table access, so granting a write can’t be escalated into DELETE FROM orders or DROP TABLE orders. They’re configurable per flag in the guardrails block if you really need to allow one.
Postgres, since 0.1.0 — it’s the only dialect supported today. The dialect: key accepts only postgres. See the policy engine.
Self-host is free — MIT open source, no hosted dependency. Midplane Cloud has a Free tier plus Pro and Team plans, gating on projects, MCP tokens, and seats rather than query volume. The policy engine, audit logging, and credential isolation are on every tier. See plans.
Self-host sends anonymous, aggregate telemetry by default — no SQL, no table or column names, no query results, no credentials. Disable it with MIDPLANE_TELEMETRY=0 or DO_NOT_TRACK=1; inspect what’s sent with MIDPLANE_TELEMETRY=debug. See telemetry.

Get help

If the fixes above don’t cover it:
Email support@midplane.ai — also the Email support link in the dashboard sidebar. A human reads and answers.
Found a security vulnerability? Don’t open a public issue — see report a vulnerability.

Still stuck?

Denial reasons reference

Every denial message Midplane returns, and what to do about it.

How Midplane works

The parse → policy → audit → execute pipeline behind every decision.