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

# Connect your database

> What Midplane needs from a Postgres connection — a reachable host, a least-privilege role, and TLS — and the per-provider specifics.

The connection string you paste into Midplane decides what an agent can reach *before* any policy applies. Policy is the ceiling; the database role is the floor. Give Midplane a role that already can't do the damage you're worried about, and the [policy engine](/docs/how-it-works) becomes your second layer rather than your only one.

This page covers what every Postgres needs to have. The provider guides below cover the parts that differ — which of several connection strings to use, and where each one hides a trap.

<Note>
  Postgres is the only supported dialect. Any Postgres works — managed, self-run, or a read replica — as long as it meets the three requirements below.
</Note>

## What Midplane needs

### A URL-form connection string

Midplane takes a `postgres://` or `postgresql://` URL:

```
postgres://user:password@host:5432/dbname?sslmode=require
```

The libpq keyword form (`host=… dbname=…`), a JDBC URL, and a pasted `psql "…"` command are all rejected with a message naming the problem. If your password contains `@`, `:`, `/`, `?`, `#`, or `%`, percent-encode it — `@` becomes `%40`.

### A host Midplane can reach

<Tabs>
  <Tab title="Cloud">
    Midplane Cloud connects to your database over the public internet, so the host must be **publicly routable**. Addresses that aren't — loopback, the private ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`), CGNAT, link-local, and IPv6 unique-local — are rejected before any connection is attempted. **Test connection** reports:

    ```
    Could not connect. Check the host, port, and that the database accepts
    connections from the internet.
    ```

    A hostname that *resolves* to a blocked address is rejected the same way, so an internal DNS name pointing at a private record won't connect either. Saving such a database isn't blocked — the connection string is only shape-checked on save — but queries against it will fail once an agent tries.

    Midplane Cloud doesn't publish a fixed egress IP range, so there's no firewall rule that admits Midplane without admitting everything else too. Don't widen a database to the whole internet to make Cloud reach it — a database that shouldn't have a public endpoint belongs on self-host.
  </Tab>

  <Tab title="Local">
    The [standalone engine](/docs/self-host/engine) runs as a process on your own machine, so it reaches whatever that machine reaches — including a development Postgres on `localhost:5432`, which needs no public endpoint at all. There's nothing to make routable.

    This is the shape for a database on your laptop or behind a VPN you're already on. It guards one database for one agent on that machine, with no dashboard — see [the standalone engine](/docs/self-host/engine) for what it does and doesn't include.
  </Tab>

  <Tab title="Self-host">
    Self-host runs inside your own network and reaches whatever its host can reach — a private VPC address, an internal DNS name, or another container on the same Docker network. There's no public-reachability requirement.

    One thing to watch: inside the container, `localhost` is the *container*. For a Postgres running on the Docker host, use `host.docker.internal` (Docker Desktop) or put the database on the same Docker network. See [deploy](/docs/self-host/deploy).
  </Tab>
</Tabs>

<Note>
  **Need Cloud to reach a database with no public endpoint?** Self-host is the supported answer today — the same engine, policy model, dashboard, and audit trail, running where your database already is.

  If self-hosting isn't workable for your team, email [support@midplane.ai](mailto:support@midplane.ai) with a sketch of your setup — which provider, what your network policy requires, and why running Midplane yourself doesn't fit. We're tracking who needs this, and that's what decides whether we build it.
</Note>

### A least-privilege role

Midplane enforces policy on top of your database's permissions — it doesn't replace them. Create a role for Midplane alone, so a policy mistake can't become a data-loss event:

```sql theme={null}
CREATE ROLE midplane LOGIN PASSWORD 'a-strong-password';
GRANT CONNECT ON DATABASE app TO midplane;
GRANT USAGE ON SCHEMA public TO midplane;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO midplane;

-- Future tables, so a new table isn't invisible to your agent
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO midplane;
```

<Warning>
  `ALTER DEFAULT PRIVILEGES` only applies to tables created by the role that runs it. If your migrations run as a different role, run the statement as that role too — otherwise tables created later won't be readable.
</Warning>

Repeat the `USAGE` and `SELECT` grants for every schema the agent should see; `public` is rarely the only one.

Granting writes takes two steps, in both layers. Marking a table `read_write` in [policy](/docs/concepts/table-access) lifts Midplane's ceiling, but the query still runs as your role — so the role needs the privilege too:

```sql theme={null}
GRANT INSERT, UPDATE, DELETE ON feature_flags TO midplane;
```

A table that's `read_write` in policy but has no `INSERT` grant will pass policy and then fail at the database. That's the layering working as intended, but the error comes from Postgres, not from Midplane.

### TLS

Add `sslmode=require` unless you have a reason not to. Since engine `0.15.0` Midplane follows libpq's semantics: `require` **encrypts without verifying** the certificate, so a self-signed or private-CA database connects. To verify, pass `sslrootcert=/path/to/ca.crt`, or use `verify-ca` / `verify-full`. See [connecting to your Postgres](/docs/resources/troubleshooting#connecting-to-your-postgres) for the failure modes.

## Point at a replica when you can

Nothing about Midplane requires the primary. If your agent's job is analysis, give it a read replica's connection string: the replica physically cannot accept a write, so a policy gap and a database gap have to line up before anything is modified. You still get the [audit trail](/docs/concepts/audit-trail) and [column masking](/docs/concepts/masking) on every query.

Reach for the primary only when the agent genuinely needs to write — and then grant it per table, with [write approvals](/docs/concepts/write-approvals) on anything you'd want a human to see first.

## Provider guides

<Columns cols={2}>
  <Card title="Supabase" href="/docs/connect/supabase" horizontal>
    Which of the four connection strings to use, and the pooler username trap.
  </Card>

  <Card title="Neon" href="/docs/connect/neon" horizontal>
    Pooled vs direct endpoints, SNI routing, and pointing an agent at a branch.
  </Card>

  <Card title="Amazon RDS & Aurora" href="/docs/connect/rds" horizontal>
    Reachability from Cloud, `rds.force_ssl`, and why IAM auth doesn't fit.
  </Card>

  <Card title="Railway" href="/docs/connect/railway" horizontal>
    Why the internal hostname fails, and the TCP proxy that fixes it.
  </Card>
</Columns>

Using something else? Every other Postgres — Google Cloud SQL, Azure Database, Heroku, Fly, Render, Timescale, Crunchy, or your own — follows the three requirements above with no extra steps. Start with the [quickstart](/docs/quickstart).
