The hub is one function
The hub is a plain(Request) => Promise<Response>:
- Locally,
Bun.serve({ fetch: handler })consumes it. - In the cloud, a Cloudflare Worker forwards to a Durable Object that calls the same handler.
Response wins. Every route
except GET /health requires a bearer token. Every response uses the same envelope as the
CLI’s --json output:
127.0.0.1 only. The cloud host
picks an auth strategy from configuration: a shared token, or JWTs validated against an
issuer’s JWKS URL with an expected audience, so you can put pinbox behind whatever
identity system you already run.
The local daemon spawns itself
You never start a server. Anypinbox command that needs the hub calls ensureHub(),
which:
1
Reads the state file
The pid, port, token, and version, from your XDG state directory (mode
0600 — secrets
never sit in the repo). The project directory only gets .pinbox/server.json, which
holds the port and nothing else.2
Probes it
GET /health on that port. A healthy hub of the right version is reused. A hub from an
older version gets a SIGTERM and is replaced.3
Otherwise spawns one
A detached
pinbox serve in its own process group, then polls until that pid’s state
file answers /health. Detached matters: a Ctrl+C aimed at the CLI must not take the
daemon with it.PINBOX_IDLE_MS overrides the timeout, in milliseconds). Idle exit is safe because the
queue is durable, not the process — the next command respawns the daemon and it replays
whatever it missed.
Storage is SQLite, twice
PinStore is the interface. There are exactly two implementations:
Both replay the same numbered migration list, as plain SQL. Nothing engine-specific
leaks through the interface, which is what keeps the two from drifting apart. Locally, an
FTS5 index over pin and thread text backs the hub’s
GET /pins?search=<query>.
The event log is the backbone
Pins and thread messages live in derived tables. The source of truth is an append-onlyevents table:
seq is a monotonic integer. Event types are pin.created, pin.resolved,
thread.message, pin.verified, and pin.linked. Each payload is the complete
post-mutation object, so a consumer can apply an event without a follow-up read.
That gives every consumer the same simple contract: remember the last seq you saw, ask
for everything after it.
- HTTP:
GET /events?after=<seq>returns the tail.GET /summarygives youlastEventSeqin one call. - WebSocket: a client connects to
/wsand sends one hello carrying itsconsumerIdandlastSeq. The hub replies with a catch-up frame containing every event since, then streams new ones live. A client that drops reconnects, sends its cursor, and loses nothing. - Delivery: every event gets a row in the deliveries ledger — including events that are
unroutable by design, which are recorded as
skipped. SoMAX(event_seq)in that ledger is the delivery cursor. On boot the daemon replayseventsAfter(lastEventSeq)and picks up anything that happened while nothing was running.
How pins reach agents
The session registry, sticky routing, and the four delivery paths.