An Oblivious HTTP (RFC 9458) gateway.
Decapsulates encrypted OHTTP requests, forwards the inner request to a target, and returns the encapsulated response. The gateway learns what was requested but never who requested it — that is OHTTP's privacy guarantee.
Keys are derived deterministically from a single master seed
(OHTTP_KEY_SEED), so the gateway is stateless and deploys the same way on
every platform: no database, no key storage. The same seed produces the same
key configuration on every instance and region.
OHTTP requires the gateway and relay be operated by different entities. We list several platforms so you can put each on a different provider: one provider operating both could attribute requests to clients.
| Platform | Runtime | |
|---|---|---|
| Cloudflare | Workers | |
| Vercel | Edge | |
| Netlify | Edge (Deno) | |
| Railway | Node.js |
After deploying, set OHTTP_KEY_SEED (see Keys). Cloudflare uses a
secret (npx wrangler secret put OHTTP_KEY_SEED); the others take an
environment variable. Netlify and Cloudflare deploy buttons do not prompt for
it, so set it in the dashboard afterwards.
Generate a master seed once and use the same value on every deployment:
npm run keygenThis prints a base64 seed and the derived key IDs. From the seed the gateway deterministically derives one key configuration containing two keys:
| Key | KEM |
|---|---|
| Classical | X25519 (DHKEM-X25519-HKDF-SHA256) |
| Post-quantum | ML-KEM-768 |
If OHTTP_KEY_SEED is unset, the gateway generates an ephemeral seed at boot so
local development works without setup. Such keys are not persisted and differ
between instances — never run production without a seed.
| Variable | Default | Description |
|---|---|---|
OHTTP_KEY_SEED |
(ephemeral) | Base64 master seed for key derivation |
TARGET_URL |
https://target.ohttp.info |
Base URL the inner request is forwarded to |
CORS_ORIGIN |
* |
Allowed CORS origin |
MAX_REQUEST_SIZE |
1048576 |
Maximum request body size (bytes) |
PORT |
3000 |
Listening port (Node.js only) |
On Cloudflare, an optional TARGET service binding to a co-located
ohttp-target Worker is used in place of TARGET_URL when present (see
wrangler.toml).
| Method | Path | Description |
|---|---|---|
GET |
/.well-known/ohttp-gateway |
Key configuration (application/ohttp-keys) |
GET |
/ohttp-config |
Alias of the above |
POST |
/ohttp |
OHTTP decapsulation |
GET |
/health |
Health check |
The Content-Type of a POST /ohttp request selects the variant:
| Content-Type | Description |
|---|---|
message/ohttp-req |
Standard OHTTP (RFC 9458) |
message/ohttp-chunked-req |
Chunked OHTTP (streaming) |
npm install
# Cloudflare Workers (wrangler dev)
npm run dev
# Node.js server
npm start
# Tests, types, lint
npm test
npm run typecheck
npm run lintClient → Relay → Gateway → Target
↑
(this service)
The gateway decapsulates the OHTTP request, forwards the inner request to the target, then encapsulates the response. It learns what was requested but never who asked: the relay has already stripped the client's identity. Keys come from OHTTP_KEY_SEED, so there's no per-request state to keep. The same seed produces the same key configuration wherever you run it.
This software has not been audited. Please use at your sole discretion.
The gateway's privacy and confidentiality guarantees rely on:
- Oblivious HTTP (RFC 9458) and its chunked extension (draft-ietf-ohai-chunked-ohttp).
- HPKE (RFC 9180), as implemented by
hpke(X25519) and@panva/hpke-noble(ML-KEM-768). - The
ohttp-tsimplementation of OHTTP. - The underlying key-encapsulation mechanisms: X25519 (classical) and ML-KEM-768 (post-quantum).
The privacy property only holds when the gateway and the relay are operated by separate, non-colluding parties: the relay hides the client's identity from the gateway, and the gateway hides the request contents from the relay.
OHTTP_KEY_SEED is the gateway's most sensitive value — it deterministically
derives every private key, so anyone who learns it can decapsulate all traffic.
Store it as a platform secret, never in source control.
Rotation is not automated yet. Because the published configuration can advertise
multiple keys and the server decapsulates by key ID, the intended approach is a
manual overlap: add a second seed-derived key, advertise both for at least the
24h configuration cache window, then drop the old one. Until then, changing
OHTTP_KEY_SEED rotates the keys immediately — clients holding a cached
configuration will fail to decapsulate until they refetch
(Cache-Control: max-age=86400).
This project is licensed under the MIT License.