A peer-to-peer mesh where everything is a fetch(). Peers — Node
processes and browser pages — serve HTTP handlers to each other over libp2p,
and a page calls another peer with its own fetch() through a ServiceWorker
edge. This repository holds both halves:
packages/— the libraries a page or a process builds a mesh from, published to npm under@statewalker/*, plus two private conformance suites.apps/anddeploy/— the services the mesh needs to exist and the pages that run on them: the circuit relay (relay.httpeers.net), the static-site host (*.httpeers.net), the session shell (*.p.httpeers.net), the S3 storage behind them (s3.httpeers.net), the ingress, the Node hub daemon with its LLM appliance, the LLM chat and the demo pages.
Before building on it, read docs/security-model.md.
The mesh mounts every peer's HTTP surface into your own origin and calls it
with your identity automatically, so data crosses that boundary safely but
code does not. That document defines the valid uses and the ones to avoid.
packages/ the libraries (below)
apps/relay/ the circuit relay, and its image
apps/sites/ the static-site host: one site per storage prefix, and its image
apps/session-shell/ the empty session every <name>.p.httpeers.net serves
apps/hub/ the Node hub daemon (identity, rules, service modules, admin UI), and its image
apps/appliance-prepare/ hardware probe and config generator for the local-model LLM appliance
apps/llm-chat/ the chat page: standalone (index.html) or over the mesh (mesh.html)
apps/demos/ the demo sites: hub, app, images, proxy
deploy/ the production compose stack, the Caddyfile, the ingress image
deploy/llm-appliance/ hub + LiteLLM + Postgres + Traefik, locally or on the server
tools/publish/ shell toolkit: publish sites by editing a folder
docs/ the security model
.github/workflows/ CI, and one image workflow per deployable
One contract runs through all of them: FetchHandler = (Request) => Promise<Response>.
A mount table routes it, a peer serves it over libp2p, an edge lets a page
reach it through its own fetch(). Only httpeers-libp2p knows what libp2p
is.
| Package | npm | What it is |
|---|---|---|
httpeers-core |
@statewalker/httpeers-core |
the handler contract, the mount router, the identity and token headers; no dependencies |
httpeers-access |
@statewalker/httpeers-access |
who is calling, and may they: Biscuit tokens, Datalog policy, revocation, one middleware; no libp2p |
httpeers-person |
@statewalker/httpeers-person |
person keys, the statements they sign, and the link code; no dependencies |
httpeers-bridge |
@statewalker/httpeers-bridge |
fetch over a duplex, both directions, with no transport in it; ./ports runs a mesh over MessageChannel |
httpeers-libp2p |
@statewalker/httpeers-libp2p |
the transport: nodes, identity, relay reservations, servePeer |
httpeers-hub |
@statewalker/httpeers-hub |
invitations, membership tokens and the registries; no transport, so a hub can run in a tab |
httpeers-member |
@statewalker/httpeers-member |
everything a participant does: startMember, the session, the edge, the gateway |
httpeers-ghost |
@statewalker/httpeers-ghost |
a remote peer's app rendered as a page that can reach only that peer |
httpeers-qr |
@statewalker/httpeers-qr |
invitations as QR: pure encode/decode, plus a browser camera scanner |
httpeers-join |
@statewalker/httpeers-join |
the join-the-mesh widget every page shares, in plain DOM |
webrun-biscuit |
@statewalker/webrun-biscuit |
Biscuit tokens in pure TypeScript: codec, signatures, Datalog, authorizer |
httpeers-conformance |
private | every entry point resolved and imported as a dependent sees it, and one real mesh |
httpeers-browser-conformance |
private | what only a real browser can answer: the ServiceWorker edge, reset, reloads |
The apps (apps/*) are all private: they ship as container images or static
sites, never to npm.
Other @statewalker/* packages used here come from npm: webrun-streams,
webrun-streams-libp2p, webrun-http-streams, webrun-http-browser,
webrun-http-proxy, webrun-rpc and webrun-files*.
Requirements: Node 24 and pnpm 10 through corepack (the version is
pinned in package.json's packageManager).
corepack enablepnpm installpnpm build— every package and app (pnpm -r run build;pnpm turbo buildbuilds in dependency order and caches).pnpm test— every package's tests. They bind real ports and start real libp2p nodes. A package'stestscript builds and typechecks it first.pnpm typecheck,pnpm lint:check,pnpm format:check.
One package: pnpm --filter @statewalker/httpeers-member test.
A relay on this machine, where the bound address is reachable and so no announce address is needed:
pnpm --filter @statewalker/httpeers-relay bootstrap # writes apps/relay/.httpeers/relay.key
RELAY_REQUIRE_ANNOUNCE=false pnpm --filter @statewalker/httpeers-relay devEach app's README says how to run it: relay, sites, session shell, hub, llm-chat, demos.
deploy/README.md is the runbook for the production host:
four containers (Caddy, relay, sites, RustFS) behind a wildcard DNS record and
wildcard certificates, so publishing a new subdomain changes nothing there.
The LLM appliance is a separate stack, described in
deploy/llm-appliance/README.md.
A push to main that touches a deployable builds and pushes its image
(.github/workflows/relay.yml, sites.yml, ingress.yml, llm-appliance.yml).
The relay and sites workflows then deploy over SSH when the repository variable
DEPLOY_ENABLED is true, and the LLM appliance workflow when
LLM_DEPLOY_ENABLED is true; the ingress is deployed by hand. The LLM
appliance workflow watches all of packages/**, so any change there — a README
included — rebuilds and redeploys the hub. On a pull request, CI runs and the
LLM appliance image is built without being pushed.
x-httpeers-peer carries the peer the transport proved. A header survives a
re-created Request — which is what handlers do — and lets the security
property be tested in plain HTTP. Because a header is whatever the caller
typed, registerPeer, registerAnonymous and stripPeerBinding all strip
before they write, and every entry point calls exactly one of them.
httpeers-conformance checks it over a real libp2p connection: a peer claiming
to be somebody else is overwritten by the handshake.
x-httpeers-token carries the bare token (MESH_TOKEN_HEADER in
httpeers-core, the only place the name is spelled). The edge, peerRequest
and the ghost write it; withAccess reads it and nothing else.
Authorization passes through the mesh untouched, for whatever application the
request is addressed to — for example LiteLLM's own API key. A proxy
re-issuing a request outside the mesh strips MESH_CREDENTIAL_HEADERS (the
token and the proven peer), never Authorization. httpeers-conformance scans
every library's source to keep it that way.
A package's root runs in Node, in a worker and in a page alike; anything that
cannot goes behind ./node or ./browser. Each package's boundary test
enforces it — in httpeers-member and httpeers-qr by walking the import
closure of index.ts, so the rule is "nothing a root import reaches", not
"these file names are exempt".
A published entry point can type-check and still fail to import, and two
packages that pass their own tests can still fail together.
httpeers-conformance compiles every entry point the way a dependent does,
imports every isomorphic one, and stands up a live relay, hub and two members
with nothing stubbed.
node-datachannelmust be allowed to build.@libp2p/webrtcneeds this native module, and pnpm 10 blocks install scripts unless a package is named inonlyBuiltDependencies(it is, inpnpm-workspace.yaml). Remove that entry and pnpm printsIgnored build scripts: node-datachannel, reports success, and@statewalker/httpeers-member/nodethen throws on import.- Apps bundle workspace packages from
dist/. The packages'exportshave asourcecondition, but the Vite builds do not enable it. After changing a package, rebuild it before building or testing an app that uses it. - Merging to
mainchanges production. See Deployment. - The word "relay" means two things. In the mesh's vocabulary a Relay is a
peer that forwards on a third party's behalf;
apps/relayis the libp2p circuit relay, a transport component.
| Command | What it does |
|---|---|
pnpm build |
pnpm -r run build |
pnpm test |
pnpm -r run test |
pnpm typecheck |
pnpm -r run typecheck |
pnpm lint / pnpm lint:check |
Biome check, with or without --write |
pnpm format / pnpm format:check |
Biome format, with or without --write |
pnpm turbo build / pnpm turbo test |
the same through turbo: dependency order and caching |
CI runs on every pull request and every push to main: a frozen
pnpm install, a check of dependency-reference conventions (workspace:^
inside the repository, catalog: for external versions), lint:check,
format:check, build, typecheck, test, and checks that every published
entry point exists in dist/, imports, and packs. The public packages are
published to npm under @statewalker/*.
MIT — see LICENSE.