Skip to main content

Mesh, transports, and self-hosting

The lattice is eighteen nodes serving one protocol. This page is the fabric that joins them and the path to adding your own.

What this page used to say

An earlier version described the mesh as a "gossip channel (zeq:mesh:gossip)" that each node subscribes to on boot, and presented that as how nodes find each other and how fleet reads work. That channel is real, but it is not the node-to-node transport — it is a Redis pub/sub bus, and an off-box node can neither reach it nor be given credentials to it. The real answer is the two transports below. What zeq:mesh:gossip actually is has its own section.

Transport 1 — ZeqSSH over raw TCP, port 18870

Replication between nodes does not go over HTTP. It rides a raw TCP session on the mesh port, with no nginx in the path. Every entry in GET /api/mesh/peers carries tcp_port: 18870.

shared/api-core/src/lib/zeqTcp.ts is the whole transport:

LayerWhat it does
Framing4-byte big-endian length, then JSON. FRAME_MAX is 1 MiB — anything larger is refused.
WireHELLO (client) → HELLOACK (server) → FRAME / ACK, the last two carrying AES-256-GCM ciphertext.
Record keysderiveRecordKey() derives a per-zeqond AES-256-GCM key by HKDF-SHA256, rekeyed every zeqond and separated by direction, so a captured frame's key never opens a later one.
Replay windowHELLO_SKEW = 300 zeqonds (~4 minutes). A HELLO further out of step is refused.
The TCP HELLO is HMAC, not Ed25519 — be clear about this

The HELLO carries an HMAC-SHA256 over its canonical fields (zeqssh-hello|sessionId|nonce|nodeId|zeqond), keyed on the shared ZEQ_MESH_SECRET. The per-session key is HKDF(ZEQ_MESH_SECRET, sessionId, nonce), so both ends derive bit-identical keys with no public-key exchange at all.

That means the shared secret is the trust boundary for everything riding this channel — which is why a mesh_announce over TCP carries no additional signature. The source says so in its own words: "Ed25519 peer identity can layer on later; the record layer is unchanged."

Ed25519 is real, and it is used — on the HTTPS peer protocol below, not here. Any document that describes ZeqSSH as carrying an Ed25519 identity is describing an intention.

Peer discovery rides the same channel. announceSelfOnce() sends a mesh_announce carrying this node's descriptor to every known peer and merges back their peer sets, so the mesh becomes fully connected from a single bootstrap peer with no environment surgery. That registry is what GET /api/mesh/peers serves.

Port 18870 is not a public front door. lib/meshJoin.ts states that the replication transport lives inside the fleet's docker network and is not reachable from off-host or behind NAT. A node on someone else's hardware uses the HTTPS join below instead. (We could not probe the port from outside the sandbox to confirm the filtering independently — this is the code's own statement.)

Transport 2 — the signed HTTPS peer protocol

Some reads have to be node-to-node over HTTPS rather than the replication channel — the home-node directory is the clearest example. Those use a separate, signature-based protocol implemented once in lib/peerAuth.ts, with no shared secret anywhere.

Three headers on every request:

HeaderContents
x-zeq-peer-originthe caller's public origin, lowercased, no trailing slash
x-zeq-peer-zeqondthe caller's zeqond at signing time
x-zeq-peer-sigEd25519 signature over a canonical string

Three checks on the receiving side, in order: the origin must be a framework-owned domain (403 not_a_peer); the claimed zeqond must be within PEER_ZEQOND_WINDOW = 300 zeqonds (403 stale_peer_request); and the signature must verify against the caller's published key, fetched from its own GET /api/node/status and cached for 60 seconds (403 bad_peer_sig).

# every node publishes the key it will be checked against
curl -s https://zeqproof.com/api/node/status \
| python3 -c "import sys,json;d=json.load(sys.stdin);print(d['nodeId'], d['publicKeyHex'])"

# unsigned, so it is refused — the gate working
curl -s https://zeq.me/api/identity/peer/presence/ZEQTEST
# {"ok":false,"error":"not_a_peer"}

The trust anchor here is the signature against a published key, never the transport. Full detail on the spine page.

What zeq:mesh:gossip actually is

zeq:mesh:gossip exists — MESH_CHANNEL in lib/meshClient.ts — and it is worth knowing what it does, because it is not what the old page claimed.

  • It is a Redis pub/sub channel, on a dedicated Redis connection (createDedicatedRedis), structurally separate from the app-cache/session Redis.
  • It carries state-machine eventsstate_tick, state_transition, machine announcements — so that observers and aggregators on a node see peer events alongside their own. It does not carry WORM replication; that is the TCP channel above.
  • It only reaches processes on the same Redis. The source is explicit that a node on another host "cannot reach that Redis and must not be given credentials to it."

The HTTP counterpart is POST /api/mesh/gossip, which is the front door for off-box nodes and is HMAC-authenticated on the x-node-handshake header:

curl -s -X POST https://zeq.me/api/mesh/gossip -H 'content-type: application/json' -d '{}'
# {"error":"Node handshake verification failed","code":"MISSING_HANDSHAKE"}

Two peer relationships, two transports: Redis is the co-located fleet's internal bus, HTTPS is the front door, and neither substitutes for the other.

The open join — how an off-box node pulls the shared log

You do not need the mesh secret to read the federated spine. lib/meshJoin.ts implements an open-contributor join over 443, priced in proof-of-compute rather than credentials:

  1. POST /api/mesh/join → a Landauer proof-of-compute challenge (429 POW_REQUIRED).
  2. Solve it — hashcash: sha256(challenge + ':' + solution) must have at least difficultyBits leading zero bits. Live on 2026-09-03: difficultyBits: 18, expiresInSec: 600, tagged with the operator that priced it (LZ1) and its Landauer energy floor.
  3. POST /api/mesh/join { pow: { challenge, solution } } → a signed join token, the full peer roster, and this node's anchor.
  4. GET /api/worm/fed/stream?after_seq=N with header x-zeq-mesh-token → the federated sub-chain.
curl -s -X POST https://zeq.me/api/mesh/join -H 'content-type: application/json' -d '{}'
{ "ok": false, "code": "POW_REQUIRED",
"challenge": "18.1788434951758.87ade8e8e68da933b492cdde6bcddc70.d5fbbd1ca834b6b10f03ed06f60c7f50",
"difficultyBits": 18, "expiresInSec": 600,
"algorithm": "sha256(challenge + ':' + solution) must have >= difficultyBits leading zero bits",
"landauerJoules": 5.167761993141703e-20, "operator": "LZ1" }

The gate allows a small free quota per caller before it starts demanding work, so a first call may return the token directly. The token is an HMAC over the fleet mesh secret, so a token issued by any node verifies on every node — one join, whole fleet — and it expires after JOIN_TOKEN_WINDOW_Z = 600 zeqonds (about 7–8 minutes).

The successful response also hands back the bootstrap roster you would otherwise have to be told:

{ "ok": true, "token": "eyJ0eXAiOiJtZXNoLWpvaW4i…", "issuer": "machine-zeqme",
"roster": { "self": { "node_id": "machine-zeqme", "base_url": "https://zeq.me",
"tcp_host": "zeq.me", "tcp_port": 18870 },
"peers": [ /* the other seventeen */ ] },
"anchor": { "node": "machine-zeqme", "ledger_root": "1f8b002a…", "ledger_count": 650 },
"hint": "GET /api/worm/fed/stream?after_seq=N with header x-zeq-mesh-token …" }

GET /api/mesh/peers is the unauthenticated version of that roster, if all you want is the map.

Certificates

Application-layer TLS is the framework's own: Zeq-SSL (lib/sslHandshake.ts, lib/sslCredentials.ts, lib/sslTrustGraph.ts) issues and manages credentials for hosted channels and fork domains. Its record layer is the same deriveRecordKey() that the TCP transport above encrypts frames with. The SSL routes live in the security reference.

Standing up your own node

A fork is a full node that speaks the same protocol. The shape:

  1. Bootstrap secrets — generated by infra/air-gapped/bootstrap.sh. They live server-side and are never checked into a repo.
  2. Run the stack — the api-core kernel plus its own Postgres and Redis, from the air-gapped docker-compose.yml. The node boots and starts its Zeqond clock and seal spine.
  3. Point a domain at it — nginx routes your domain's root to the node; /api/… is the live surface, /s/… serves hosted channels.
  4. Join and pull — the proof-of-compute join above gets you the roster and a token for /api/worm/fed/stream. Your node verifies every row it applies rather than trusting the sender.
  5. Verify parity — the acceptance test is the one from Nodes: your node's GET /api/endpoints hashes identical to the reference fleet.
What a fork does not get

Joining gets you read access to the federated spine. It does not make you the issuer — exactly one node seals issuance, and the other seventeen production nodes run with ZEQ_MESH_ISSUER=false (the issuer). It also does not replicate anyone's private state: machines, envelopes, coins, pages and vault secrets never enter the wire from any node (what travels).

Because verification travels but private state doesn't, a fork is a peer, not a replica: machines spun up on your node live on your node, and their proofs verify anywhere — including back on the reference fleet.

What stays the same everywhere

The kernel, the operator registry, the constants, the clock and the proof formats are identical on every node by construction — that's what makes a ZeqProof from one node checkable on another. A fork customises its surface (domain, branding, which channels it hosts), never the maths.