Deployment¶
The hub is the only long-running piece — workers and human clients connect to it. Run one hub per coordinating group.
Local, always-on (systemd user service)¶
The local-first default: a per-user service so the hub is always up and restarts on login, with no root.
pipx install synapse-channel
mkdir -p ~/.config/systemd/user ~/synapse
cp deploy/synapse-hub.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now synapse-hub
systemctl --user status synapse-hub
The hub then listens on ws://localhost:8876, persists to ~/synapse/hub.db, and
mirrors the channel to ~/synapse/feed.ndjson. To survive a full logout (no
session open), enable lingering once: loginctl enable-linger "$USER".
Provider-independent presence¶
An agent's wake loop (a backgrounded synapse wait) gives prompt wakes, but it
dies with the agent — so when a turn-based assistant is down or its API is rate
limited, the project drops off the roster. Decouple reachability from the agent
with a presence holder: a per-project systemd template that holds the hub
connection and is restarted by systemd if it ever dies.
cp deploy/synapse-presence@.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now synapse-presence@myproject
It registers as myproject-presence, costs nothing (it holds a socket — no model),
and keeps the project visible in synapse who and addressable even while the agent
is offline. No message is lost meanwhile — the hub records them durably — so the
returning agent catches up with synapse relay --project myproject (or the
syn-resume helper). The two layers are complementary: the presence holder is
always-on reachability; the wake loop is promptness while the agent runs.
Presence is not a wake. The presence holder keeps the project in the roster and the feed durable, but it does not wake the agent — only an active
synapse waitdoes. If the agent stops re-arming its waker and leans on the presence daemon alone, it stays reachable but nothing wakes it: messages wait until the next manualsyn-inbox/syn-resume. Keep the waker running for promptness; the presence daemon is a safety net for reachability and durability, not a substitute for it.
Container¶
docker compose up -d # builds the image and starts the hub
docker compose logs -f hub
The compose file publishes the port on 127.0.0.1 only and stores the durable log
in the synapse-data volume. Build and run the image directly if you prefer:
docker build -t synapse-channel .
docker run -d --name synapse-hub -p 127.0.0.1:8876:8876 -v synapse-data:/data synapse-channel
On a release the docker workflow publishes ghcr.io/anulum/synapse-channel.
Exposure and security¶
The hub binds loopback and runs unauthenticated by default — correct for one
operator on one machine. Before exposing it beyond localhost:
- Bind off-loopback only with a shared secret:
synapse hub --host 0.0.0.0 --token "$SYNAPSE_TOKEN". The hub warns when bound off-loopback without a token. - In compose, change the port mapping to
8876:8876and setSYNAPSE_TOKEN(uncomment thecommand:block). Clients then pass--token "$SYNAPSE_TOKEN". - The token is a proportionate gate (constant-time check), not a cryptographic identity system; put real network controls in front of a multi-host hub.
Persistence and backups¶
With --db, every authoritative mutation (claims, releases, task updates, chat)
is written to an append-only SQLite event log in WAL mode, and the hub rebuilds
its state by replaying it on start-up. Back up the hub by copying the --db file
(and its -wal/-shm siblings) or the whole data directory while the hub is
stopped, or use sqlite3 hub.db ".backup" online. The --relay-log feed is
derived state and bounded by --relay-max-lines; it is safe to truncate.
Restarting the hub safely¶
The hub restarts cleanly because both ends are built for it. With --db, a restart
replays the event log, so active leases are restored rather than dropped. On the
client side a waiter on 0.28.1+ exits with code 3 when its socket drops instead
of hanging on a dead connection, so a hub restart makes every waiter exit and re-arm
rather than go dark.
When a waiter re-arms right after its process was killed, its old name can still
linger on the hub for a few seconds (until the keepalive reaps it). A 0.29.0+ client
re-arms with takeover: the hub evicts the stale holder (closing it with code
4010 superseded) and rebinds the name, so the re-arm succeeds instead of failing
with a 4009 name conflict. Takeover needs both ends on 0.29.0+ — the client to
ask for it, the hub to perform the eviction — and a 15-second keepalive reaps a
genuine ghost quickly as the backstop.
So a coordinated restart is safe when every live client is on 0.28.1+: announce, restart the service, and the fleet re-arms against the fresh hub on its own. Pick a quiet moment, announce before and after, and never start a restart that would strand a client too old to exit-on-drop.
Fleet-wide announcements¶
A broadcast (--target all, a --priority message, or any CEO message) wakes
every waiter at the same instant; their agents then all re-invoke and call the model
provider together, and the provider's request-rate limiter throttles the burst —
Anthropic's API, for one, returns "Server is temporarily limiting requests", a
request-rate limit distinct from your usage quota. Two defences, used together:
- Receiver side:
synapse wait --wake-jitter(default 8s) spreads broadcast wakes over a few seconds so the re-invocations do not land at once. - Sender side: to roll an update out to a fleet, do not
--target all. Send directed and staggered — one message per terminal, a few seconds apart — so the wakes are spread regardless of each waiter's jitter setting:
for p in api-dev test-dev docs-dev; do
synapse send --target "$p" "upgrade to 0.30.0: pipx upgrade synapse-channel; restart your waiter"
sleep 5
done