midplane gateway enforces what a project in Midplane Cloud publishes:
policies authored in the dashboard, agents’ identities, approvals, taint shared
by every instance, and a query log. The gateway opens every connection; the
cloud never connects to it, and it serves nothing for the cloud to call.
main) are the ones the project’s databases use in the
dashboard.
Enrollment
A project manager makes an enrollment token on the project’s Gateways page. It works once, within 24 hours, and pins Midplane Cloud’s signing key: the gateway refuses an answer signed with any other key, so a TLS-inspecting proxy can’t stand in for the cloud.- On a disk: with
link.identity: { file: identity.json }, the first start enrolls with the token and writes the identity (mode 0600). Later starts read it and never need the token again. - Without a disk (a platform that rebuilds containers): run
midplane enroll --config midplane.yamlonce. It prints the identity as one line of JSON; store it in your secret manager and pointlink.identity: { env: MIDPLANE_IDENTITY }at it.--out <file>writes it to a file instead.
public_urls (or
the listener’s) as one of the gateway’s URLs (hosted agents).
Bundles
A bundle is a project’s complete policy, signed by Midplane Cloud when someone publishes. The gateway takes one only if its signature, issuer, project and version check out, and only if it is newer than the one it holds.- Enforcing: the newest bundle is written to
link.bundle_cachebefore it takes effect. After a restart with the cloud down, the gateway enforces it again. - Waiting: a gateway that has never received a bundle serves nothing (503).
- Halted: an authentic bundle the gateway can’t fully enforce (a newer
format, a policy feature it doesn’t know, masks without
mask_salt) halts it: every call is refused, and the dashboard and the log say why. Upgrade the gateway, or fix its config. - Paused: a paused project’s gateways refuse every call until it’s resumed.
Databases it cannot reach
From0.22.0, a database the gateway can’t reach at start (a wrong host or
port, a wrong password, a database that doesn’t exist yet) doesn’t stop a
linked gateway. It enrolls, syncs and serves its other databases, and:
- Refuses every call to it, before anything else is checked or recorded,
with
Database "<id>" can't be reached from the gateway right now. - Keeps trying it, after 1 s, 2 s, 4 s and so on, at most every 30 s. Each try waits up to 10 s to connect and 30 s to read the catalog. Once one succeeds the database is served, its catalog goes up at once, and the dashboard hears within seconds.
- Reports it in its status: for each database, whether the most recent
attempt to reach it answered, since when, and if not, its SQLSTATE or Node
error code. Attempts are catalog reads, statements, the dashboard’s
connection test and
/readyzpings. A statement counts only when it can tell: one Postgres answered, even with an error, says the database is up; one that never got an answer, or lost its connection, says it’s down; one the gateway refused itself says nothing. Until a database’s catalog has been read, only a catalog read can say it’s up.
database unreachable), since
it can name roles and hosts; only the code goes up. The Gateways page and
the setup step show it beside the database: shop · can't connect: password authentication failed (28P01).
A database the gateway has read once keeps its catalog if it goes down later:
calls to it fail with the connection’s error, and its health shows the code
after the next one, until a statement gets an answer again.
/readyz answers
503 while any database is down or unread (operations).
Local mode still refuses to start without every database, and so does any
gateway with a config error (an unknown key, a missing secret file). Before
0.22.0, a linked gateway did too: it exits at start with the database’s
error, before it enrolls.
What goes up
Over the link, a gateway sends its status (bundle version, state, version, features, database ids, URLs, each database’s health as an error code), each database’s catalog (table and column names and types, view definitions with every literal replaced, never a value), held writes for approval, taint records, and its audit log (audit). It never sends a DSN, the salt, a Postgres error message, or a row. The cloud’s ack of each audit batch is signed like its other answers, for a nonce the gateway sends, and names the hash of the event it stored at the acked sequence, the hash of the request body it received, and the batch’s first event it already holds under another hash, if any: it stores a batch only up to there. The gateway stops owing events only on an ack that verifies for the bytes it sent, and only up to such a conflict, sending the rest as a new instance, so a proxy can’t make it forget events the cloud never got, whether it forwards part of a batch or stores a forged copy first. The event hashes the cloud holds are keyed with a secret that stays in the audit file, so it can’t test a guess at a statement’s values against them.Replicas
Several processes may share one identity (the sameMIDPLANE_IDENTITY), each
with its own audit file. They share approvals and taint through the cloud: an
agent tainted on one is tainted on all. On one cloud instance, replicas of one
identity take turns holding the long poll, so each syncs about once a second.
They take turns pushing audit batches too: one turned away (a 429) tries again
a few seconds later, and doesn’t log it as a failure unless the 429s go on
for two minutes. Give each replica a file of its own: a copied file goes on
as a new instance once the cloud names the other’s event at one of its
sequences. Local processes may share a file; linked ones may not.
Revoking
Revoking a gateway on the Gateways page cuts its link: it can’t get new bundles or file held writes. It keeps enforcing its last bundle, and personal access tokens it already accepts keep working there until they expire, so stop the process as well. Tested byapps/gateway/test/link.e2e.test.ts (enrollment and the pin,
bundles, halting, pausing, restarts with the cloud down, revocation, no
inbound connection, a database unreachable at start, then served, then
dropped), against a stand-in for the cloud, apps/gateway/test/catalog.e2e.test.ts
(no DSN or Postgres message in anything sent), and CI’s package job
(enroll and gateway from the npm package).