Postrust 2.3.2
Updates rustls to 0.23.46 and quinn-proto to 0.11.18, which fix seven RustSec advisories published since 2.3.1, among them a remote peer able to panic quinn-proto and a Debug impl in rustls that printed TLS secrets. Nothing in Postrust’s own code changed; the images and binaries are rebuilt against the new lockfile. The changelog lists each advisory.
2.3.1: the Cloudflare Workers adapter is released as postrust-worker 1.0.0, and keeps semver from here, on a version of its own. Its promise covers what a deployment sees rather than a Rust API: the variables it reads, the bindings it looks for, its endpoints and defaults. Nothing else changed; the other crates and the images carry 2.3.0’s code as 2.3.1.
2.3.0: the Cloudflare Workers adapter serves the REST API over Hyperdrive, on the server’s own request pipeline. A write the response refuses is rolled back, as PostgREST rolls it back. And two GraphQL fixes, found by running two Postrust subgraphs behind Apollo Router for a post on Federation, first cut as 2.2.1 and released here instead. No breaking change: cargo semver-checks against 2.2.0 passes for every stable crate. 2.2.0, below, is the release that made AWS Lambda and a NOINHERIT authenticator work.
Postrust 2.3.0
Added in 2.3.0
The Cloudflare Workers adapter works
postrust-worker 0.7.0 serves the REST API in a Worker and reaches PostgreSQL through Hyperdrive. Every request runs in a transaction that switches to the token's role and sets its claims, as on the server; the schema cache lives in Workers KV and is refreshed by a Cron Trigger or an authenticated POST /_/reload. It scores 1422 of PostgREST's 1499 conformance cases in workerd. REST only.
postrust-engine, the request pipeline with no web framework
The REST pipeline came out of the server: handle(&state, request) takes an http::Request and returns an http::Response. The server is an axum wrapper around it, and the Worker runs the same code. Server throughput is unchanged, measured on pinned cores, and the binary is 1% larger.
A driver-neutral database interface, and statement pipelining
postrust_core::db's Db and Tx traits are implemented by sqlx's pool and by PgSocketDb, which speaks the wire protocol through tokio-postgres over any socket. A differential test suite holds the two to identical answers. Over the socket driver a plain read is one flight to the database after connecting, where it was five, and a write two: from Singapore to a database in Delhi, through Hyperdrive, a read went from 380 to 240 ms.
Fixed in 2.3.0
A write the response refuses is rolled back
Refusals that can only be made after the statement has run — more rows than Prefer: max-affected allowed, several rows where one object was asked for, a PUT whose body names another row, a function setting an invalid response.status or response.headers — were made after the transaction had been committed, so the client was told the request failed while every row stayed changed. PostgREST rolls these back, and now so does Postrust, on either driver.
A schema reload no longer deadlocks a request in flight
A request held the schema cache's read lock from planning to the end of its query and took it again along the way. A reload (NOTIFY pgrst) queued in between waited on the request while the request waited on the reload, and every request after them queued behind both until the server was restarted. The lock is now released before the query runs.
A write refused at COMMIT is reported as what it is
A deferred constraint fails at COMMIT, and that was answered 500 Service temporarily unavailable, as if the pool had failed. It is now the database's error with its SQLSTATE: a deferred foreign key violation is a 409, as an immediate one is.
A header a pre-request function sets reaches a read
db-pre-request can set response headers and status on any request, and PostgREST honours them on a GET — a Cache-Control, say. They were read back only after a function call or a write.
__typename inside a relationship or an aggregate
Asking for __typename anywhere below the root — on an object or array relationship, the rows a mutation returned, or inside an aggregate's functions such as max { __typename id } — failed the whole query with column "__typename" does not exist; only the root and aggregate nodes answered it. Apollo Client adds __typename to every selection set by default, and a Federation router asks for it on every entity reference it follows, so a query that crossed from one subgraph to another through a relationship failed. It is now answered by the schema, as it is at the root.
A GraphQL token that does not verify is refused
A token signed with the wrong key, expired, or not a bearer token at all was served as the anonymous role over GraphQL, where REST refuses it with 401. The client kept getting answers, as someone else, with nothing to say why its rows had gone. It is now refused as invalid-jwt, as Hasura refuses it, and a verified token naming no role on a server with no anonymous role as jwt-missing-role-claims. With no JWT secret configured the Authorization header is still ignored, so a router forwarding it to every subgraph is unaffected.
Upgrading: nothing to change, unless a GraphQL client relied on an expired or invalid token being answered anonymously; it now gets an invalid-jwt error and should drop the token or refresh it. A db-pre-request function that sets response headers now has them applied to reads too, as PostgREST does. The public Rust API gains a crate, postrust-engine, and postrust_core::db, and loses nothing. Conformance is unchanged: PostgREST 1424/1499, Hasura 454/468. The README, docs and site also stop saying what nobody measured — “serverless-first”, “5–20 ms cold starts”, a ~5 MB binary.
Measured, not asserted
These are the figures from the most recent recorded runs: PostgREST on 2026-10-11 at commit 98ec7a1, Hasura on 2026-10-11 at commit 98ec7a1. This page reads them from the run’s own record, so they move when the suites are re-run and not otherwise. Both were run on this release’s code, and score what 2.2.0 scored. Through the socket driver the Worker uses, PostgREST’s suite agrees on every case but one, a function that asks whether it runs as a named prepared statement; see the conformance notes.
Neither harness interprets a test expectation. The reference implementation’s live response is the oracle, so a mistake in the extractor shows up as a case both servers answer the same way rather than as a false failure. Of the 456 Hasura cases counted at that level, 325 agree about data and 131 agree because both servers refuse — a distinction worth keeping, since counting only the first would score a case where Hasura itself raises an error as a failure to match it.
Federation is measured by a third suite that is not ours: Apollo’s subgraph compatibility suite, run against a fixture pinned to Apollo’s own schema and data. Its required capabilities gate the build. The optional ones Postrust does not yet support are listed under Known gaps below rather than left for someone else to find.
Postrust 2.2.0
The front door, measured
Three suites, none of them ours. HTTP Garden, a differential fuzzer that sends a payload through a proxy and shows how a set of origin servers parsed what came out, is what found the hop-by-hop defect. h2spec speaks HTTP/2 at the listener. Autobahn is the reference WebSocket suite.
The Autobahn figure needs its method stated, because the obvious number would be misleading. Postrust splices two upgraded byte streams and never parses a WebSocket frame, so most of what the suite scores belongs to the origin behind it, not to the proxy. Every run therefore has a twin that bypasses the proxy entirely, and the figure above is the difference: cases that are worse through the tunnel than without it. Of 517 cases, 1 fails — and fails identically with no proxy in the path, so it is the origin’s.
One family of cases is excluded from that comparison and named on every run rather than dropped quietly: those that send a valid message, then an invalid frame, and expect the echo of the first. The origin fails the connection without flushing that echo when both arrive in one read, and any relay coalesces what a client chopped — measured directly, with no proxy involved, the same bytes sent octet-wise echo and sent as one write do not. Which member of the family trips varies between runs, so the 8 of them are left out of the OK count above rather than making it move between runs. On this run none of them was worse than the baseline.
What 2.2.0 means
A minor release. The public Rust API gains postrust_server::build, App and Host, a graphql feature and schema_cache::enum_values, and loses nothing: cargo semver-checks against 2.1.0 passes for every stable crate. Everything else here is behaviour you see by running Postrust.
postrust-proxy and postrust-worker stay at 0.6.0: neither changed.
Added
postrust_server::build, for serving the server's router elsewhere
Everything between a loaded configuration and a router answering requests — the pool, the schema cache, REST, GraphQL and the layers around them — is a library function, with a Host saying what the host can do beyond answering requests. The binary and the Lambda adapter both serve what it returns. A graphql feature on postrust-server serves GraphQL without the admin UI, which admin-ui still implies.
The Lambda adapter is tested where it runs
A CI job builds the function with cargo-lambda and runs it in AWS's own provided:al2023 image, which carries the Lambda runtime emulator, against PostgreSQL with row-level security and a NOINHERIT authenticator: filters, writes allowed and refused, per-user rows over REST and GraphQL, functions seeing the token's role and claims, and the table state the writes leave behind.
Both came out of checking what the website said about serverless before a post repeated it: #48 for Lambda and #49 for the authenticator. How to set the authenticator up is in the authentication docs.
Fixed
The Lambda adapter serves the server's router
It carried a request path of its own, and nothing ran it. That path never bound a query's parameters, so every filter and every write failed with bind message supplies 0 parameters; and it never switched to the token's role or set its claims, so a function connected as a superuser answered every caller with every row, row-level security or not, including callers whose token named a role that does not exist. It now hands the server's router to the Lambda runtime: REST, GraphQL with Federation, JWT, the role switch and the claims behave exactly as they do in the binary.
A NOINHERIT authenticator sees the schema
The authenticator PostgREST recommends logs in with no privileges of its own and becomes the token's role for each request. The schema cache read information_schema, whose views show a connection only what it can use without switching role — through that authenticator, nothing — so every table was not found, on the server and on Lambda alike. Tables, columns, write permissions, data representations and GraphQL enum tables are now read for the roles the connection can become. Under a superuser or an inheriting authenticator the schema cache is unchanged, compared table by table and column by column over PostgREST's fixture schema.
Enum labels and table comments come from the right object
Enum labels were looked up by type name alone, so same-named enums in two schemas could swap labels, and PostgREST's own fixture — which declares an enum named bit — had its bit(5) column reported as an enum of one and two. A table's description was looked up by OID alone, so a function that happened to share the table's OID lent it its comment. Both are now looked up on the object itself.
The documentation stops promising a Workers adapter
The docs and website described Cloudflare Workers support with Hyperdrive that compiles to WebAssembly. postrust-worker is a stub that answers that it is a stub, and does not currently build for WebAssembly; the pages now say so, and what it will take.
Upgrading
If you run the server, nothing is required. If you switched your authenticator to INHERIT to get past “Could not find the table”, you can switch it back:
CREATE ROLE authenticator LOGIN NOINHERIT PASSWORD '...';
GRANT web_anon, web_user TO authenticator;If you run the Lambda adapter, its paths move to where the server has them: tables under /api, GraphQL at /v1/graphql. Set PGRST_COMPAT_MODE=true to serve the REST surface at the root as well; the old root paths only ever answered unfiltered reads. The pool is two connections per sandbox unless PGRST_DB_POOL_SIZE says otherwise, and the function must not connect as a superuser.
If you depend on the crates, nothing changes; there is only more to call.
Known gaps
On Lambda: no GraphQL subscriptions, since a function cannot hold a WebSocket, and no PGRST_DB_CHANNEL_ENABLED schema reload, since a frozen sandbox cannot listen; a new deployment or a cold start loads the schema afresh. Cloudflare Workers is not supported yet.
In subscriptions: a connection keeps the identity it was opened with, so a token that expires while a subscription is open does not end it — a client that needs that should reconnect with a fresh token. A live query on a table without a trigger re-reads on PGRST_SUBSCRIPTION_REFRESH (30 seconds by default) rather than when a row changes.
In Federation, unchanged from 2.0.0: the primary key is the only @key, so repeatable keys and nested keys are not expressible, and neither are extended or external fields, @requires, @provides, @override, @tag, @inaccessible, @composeDirective, @interfaceObject or federated tracing. Apollo counts these as optional capabilities; the compatibility fixture keeps them in its target schema on purpose, so they show in every report rather than being dropped from it. The suite’s @shareable check also reports a failure: it looks for the directive on the type, and Postrust puts it on each non-key field, which composes the same way.
In the dialects, unchanged since 1.0: the largest gap is introspection, and it is not reachable from here — async-graphql builds its own registry and keeps it private, so the directives it installs and the order it lists types in cannot be changed from outside the library. Beside it: _stream subscriptions, the cursor-based half of Hasura’s subscription surface; and the OpenAPI document PostgREST serves at /.
In the proxy, also unchanged: no HTTP/3, no upstream HTTP/2 over TLS, no response cache, no retry or circuit-breaking, no configuration reload, and automatic certificates are HTTP-01 only, so no wildcards. The FINDINGS.md files record the rest.
Previous releases
2.1.0
Row-level security sees the same identity over GraphQL as over REST, subscriptions included. The verified JWT claims reach GraphQL transactions, and a WebSocket is authenticated from its connection_init payload as an HTTP request is. Subscriptions over a WebSocket had not worked on the server as shipped; they do. Thanks to @CBeardSafire, #45.
The documented per-claim policies were rewritten to hold on pooled connections, where a setting a connection once held reads as an empty string rather than NULL.
2.0.0
Postrust can be an Apollo Federation v2 subgraph. Opt-in with PGRST_GRAPHQL_FEDERATION=true: _service and _entities, a table’s primary key as its @key, shared entities, a type prefix for composing several Postrust subgraphs, and columns exposed as GraphQL ID. Apollo’s compatibility suite gates the build. Thanks to @CBeardSafire, #42.
A major version for the Rust API only: Federation added public fields to configuration and schema structs, which are now #[non_exhaustive] so the next such feature is a minor release. For the server, the one change to notice was that a malformed boolean setting stopped startup instead of reading as false.
1.0.1
The server could not talk to a hosted database over TLS, and did not say so. sqlx was built without any TLS feature, so sslmode=require failed outright, and the default sslmode=prefer silently fell back to cleartext — every 1.0.0 deployment against a hosted database was unencrypted on the wire unless it had asked for require and noticed the error. Fixed with rustls verifying against the host trust store, so a private CA works without rebuilding, and a test keeps the feature enabled.
rustls moved to 0.23.45 for RUSTSEC-2026-0285, which enabling TLS in sqlx had made reachable from the stable line. Thanks to @CBeardSafire, who found the TLS problem and sent the fix in #38.
1.0.0
Seven crates took a semver promise: a breaking change to the public Rust API needs a major bump, which is why the Federation release was 2.0.0. postrust-proxy and postrust-worker moved to their own 0.x lines, and nothing in the stable line depends on either.
No code changed from the beta. The throughput figures were restored, measured on dedicated bare metal behind a gate that rejects a run the machine drifted under, and both conformance suites were re-run from a clean tree. The work before it made the HTTP proxy runnable and testable — automatic certificates, host routing over HTTP/2, domain verification that proves something, and a declared minimum Rust version of 1.88.
The full history is in CHANGELOG.md.
Try it
docker pull postrust/postrust:2.3.2Subscriptions are at ws://<host>/v1/graphql/ws, over graphql-transport-ws or the older graphql-ws, with the token in the connection_init payload. The policies that read the claims are in the authentication docs.