heliograph

The relay

The only transport that needs no estate infrastructure at all. No git host, no storage account, no VNet, no inbound rule. Both sides dial out over ordinary HTTPS and meet at a server neither of them trusts.

Status, plainly

station sidecomplete. Fetches requests, publishes status and progress, delivers the finished log
relay serverdbhq-uk/heliograph-relay, deployed
control sideimplemented in internal/transport, and no CLI command can select it

So it is not usable end to end yet, and this page describes it as designed so the design can be reviewed. Teaching heliograph init to select it is on the roadmap; until then, every example on this page is station-side configuration.

The threat model, which is the whole point

The relay is outside the trust boundary in both directions.

A relay that could read your logs would be a privacy problem - unacceptable for regulated customers, who are the customers.

A relay that could forge a request would have code execution inside every estate at once, through a channel the estate installed deliberately. That is the one that matters, and everything below exists to make it impossible rather than merely against the rules.

How that is achieved

Content is encrypted with a key held only on control and station. The relay stores and forwards ciphertext it cannot read, and every message is signed by its origin and verified before it is acted on.

Sign then encrypt, in that order: the signature travels inside the encryption, so the relay cannot see who signed what, and cannot strip or swap a signature it cannot reach.

Nothing bespoke:

key agreementX25519
key derivationHKDF-SHA256
encryptionChaCha20-Poly1305
signingEd25519

That is the age construction with signing added. ChaCha rather than AES-GCM because station hardware is unknown and may lack AES-NI, where ChaCha is both faster and constant-time in software.

Every message binds the estate, the station, the direction, the kind and a sequence number inside the signature, so a relay cannot replay a message, reflect one back, or forward one from a different estate.

Sequence numbers are the replay defence

They are persisted on both sides. Losing that state is not merely inconvenient: a reset would let the relay replay everything it has ever seen. The station keeps them in a local, gitignored file.

Two tokens, two scopes

Because the station token sits on a machine you do not trust and cannot reach.

tokenmay
stationread requests, write status and logs, for one estate
controlwrite requests, read logs, for one estate

Why this one transport needs a binary

heliograph-seal, and it is the single exception to nothing is installed on the far side. It is argued for explicitly rather than smuggled in.

openssl enc refuses AEAD ciphers outright. A shell implementation would have to hand-assemble encrypt-then-MAC and key agreement across openssl 1.1.1 and 3.x behaviour differences - which is where crypto bugs live, and where they are silent.

So heliograph-seal does the sealing and no networking at all. curl stays in the shell, where its behaviour can be read and debugged. Every other transport stays pure bash and always will.

The binary must be present and executable or the station refuses to start: there is no plaintext fallback and no degraded mode.

The checksum is only enforced if you set one. With RELAY_SEAL_SHA256, a binary that does not match refuses to run. Without it the station prints a warning and carries on, which is weaker than this page used to claim. Set it.

Configuring a station

TRANSPORT=relay
RELAY_URL=https://relay.heliograph.dbhq.uk
RELAY_ESTATE=payments
RELAY_STATION=sql01
RELAY_TOKEN=<the station-scoped token>
RELAY_IDENTITY=/path/to/station.key      # this station's key
RELAY_PEER=/path/to/control.pub          # the control side's public identity
RELAY_SEAL_SHA256=<checksum from the release>

RELAY_SEAL_SHA256 is optional and should not be - see above.

A request may not set TRANSPORT, PUSH, REDACT or LOG_DIR. See security.

Self-hosting

The same server, one container, no keys:

docker run -p 8080:8080 \
  -e HELIOGRAPH_RELAY_ESTATES="payments:$CONTROL_PUB:$STATION_PUB" \
  ghcr.io/dbhq-uk/heliograph-relay:latest

It holds public identities so it can route, and nothing it could decrypt anything with.

Long-poll, not WebSocket

A station runs behind a corporate proxy that may strip the upgrade header, and a transport that fails on those estates fails on exactly the estates this is for. The long poll holds for 25 seconds server-side, so a client must wait longer than that or it times out its own successful poll.

Known limits, before you rely on it

Two, both found by adversarial review and neither yet fixed. They are here rather than in an issue tracker because somebody evaluating this transport needs them before they choose it.

A cancelled run's partial log does not arrive. The station passes it alongside the cancellation status, and only the git transport honours that. On the relay the cancellation arrives and the partial evidence stays on the station.

Sequence numbers can collide between the loop and the runner. The loop loads its counter once at start; the runner delivers the finished log from its own process and advances the counter there. The loop does not reload before publishing idle, so it can emit a number the runner has already used - and the receiver drops anything at or below what it has already accepted, by design, because that is the replay defence. One of the two messages can be lost.

What DBHQ can and cannot claim

Can: the hosted relay cannot read your content, and cannot cause a station to run anything. The server source is public precisely so that is checkable rather than trusted, and it holds no keys.

Cannot: that the relay learns nothing at all. It sees message sizes, timings, which estate is active and when. Traffic analysis is not addressed and is not claimed to be.

The full account is in docs/specs/2026-09-06-relay-encryption-design.md, including what was corrected in it and why.