Skip to content

Teams

Run your own team sync server

Self-host the relay that keeps a team's workspaces in sync — written from a real end-to-end run, including the parts that are still hands-on.

On this page

Beta. Excellent is local-first: every person's app keeps a complete copy of the workspace on their own machine. To keep those copies converged, your team runs one small always-on server — the relay. You host it. Excellent never hosts it and never sees your data.

This page was written by provisioning a real relay in Docker and running real nodes against it — first on 2026-09-23, then re-run end to end on 2026-09-24 after the team-join fix landed. Every command and every error below was executed. Where a path was read in the product source but never run, it says so instead of implying otherwise.

Read this before you start

A second device can now join an existing workspace, and it works from the command line only. On the 2026-09-24 run, a clean second node received the first node's rows on its first tick, and its own rows reached the first node — both directions, with no hand-edited workspace id and no hand-edited cursor file. It is still beta, and two things will surprise you if nobody says them first.

1. The in-app switch cannot join an existing workspace. Settings → Team → Primary & sync takes a relay address but no workspace id, so a node configured that way is always the owner of its own workspace. A teammate joining yours runs excellent-mcp sync setup --team-bundle in a terminal. See §5.

2. Adding a teammate is a hand edit of a file on your server. There is no roster API and no invite link. They send you one line, you paste it into devices.json, and the relay picks it up without a restart. Removing the line revokes them the same way.

All of this is newer than the published CLI. The join commands (sync setup --team-bundle, sync invite-team, sync workspace-id), the roster hot-reload that removes the restart, and GET /health all landed on 2026-09-24. Check before you start, on every machine and on the relay, because the relay installs the CLI from npm too:

excellent-mcp sync            # the subcommand list must include workspace-id and invite-team
curl -s http://<relay>:9700/health   # must answer {"ok":true,"service":"excellent-relay",…}

If either check comes back the old way, that install predates the fix: a second device on it will pair, sync, and pull zero rows, and roster edits will not take effect until you restart the relay. Upgrade first — see §9.

What syncs, and what does not

The relay carries your workspace. The sync registry classifies 127 tables as team-shared, merged conflict-free so two people can edit the same record, online or offline, without losing anything. Genuine conflicts — money, pipeline stage, lifecycle status, archival, a person's identity — are surfaced for a human instead of silently resolved. Project rows were what the test run exercised: the owner's project reached the joining node on its first tick, and the joiner's project reached the owner.

Verification evidence stays on the machine that produced it. verification_runs is local-only. Gate-run receipts, DoD judgment receipts and the webhook delivery records are excluded from the team exchange. The entire Universal Verification store — attempts, evidence snapshots, run manifests — lives in a separate database that the sync registry does not govern at all, and evidence blobs over 1 MiB are written to local disk. Searching all 127 team-synced table names for verif|receipt|attempt|evidence|gate|proof returns exactly one hit: dod_criteria, the definition of done, not any receipt.

A teammate can see that you shipped something. They cannot open the proof behind it from their own laptop. If that is what you need, this server does not provide it. See Receipts for what a receipt binds and how to verify one offline.

1. Prerequisites

Every machine that people edit on must be an Apple-Silicon Mac. The conflict-free merge engine (cr-sqlite) is vendored for darwin-arm64 only. On any other platform it throws rather than half-loading, and the in-app switch refuses with platform-unsupported. This is a hard limit, not a warning. CRSQLITE_EXT exists as an escape hatch for an extension you build yourself; no binary for any other platform ships.

The relay host does not need to be a Mac. It is a plain Node HTTP server and never loads the merge engine. Verified: the relay ran in a node:22-slim Linux container for the whole run.

On the relay host you need:

  • Docker with Compose, or Node and the published CLI.
  • Outbound access to the npm registry. There is no prebuilt relay image — the container runs npm i -g @excellent-so/cli on every start, which took about 24 seconds in testing and is the relay's only hard external dependency. A registry outage breaks every restart.
  • One TCP port reachable by every teammate (9700 by default).
  • A non-empty allowlist file. The relay exits with status 2 on an empty roster.
  • If it crosses the public internet: a domain name and the ability to set its DNS.

On each teammate's Mac: Excellent installed, and Node 22. The run started on Node 24 and every command that opens the database failed:

$ node -v
v24.20.0
$ excellent-mcp sync pair
Error: The module '.../better-sqlite3-multiple-ciphers/build/Release/better_sqlite3.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 127. This version of Node.js requires
NODE_MODULE_VERSION 137.
  code: 'ERR_DLOPEN_FAILED'

The product pins node: ">=22.0.0 <23" and its .nvmrc says 22.23.2. Everything below ran on Node 22.23.2.

EXCELLENT_DATA_DIR is the one knob that moves a node's data directory; it defaults to ~/.excellent.

2. What it costs

Excellent charges nothing for this — no per-seat fee, no licence check. Your cloud provider bills you directly. AI features run on each teammate's own Claude Code subscription, so there is no central AI bill either.

Provider list prices as the product records them, checked 2026-06. Confirm them in your provider's console; providers change prices.

Where it runsSizeEstimated USD/month
A machine you already run (Docker)—$0
DigitalOcean1 vCPU · 512 MB$4
DigitalOcean (provisioner default)1 vCPU · 1 GB$6
DigitalOcean1 vCPU · 2 GB$12
DigitalOcean2 vCPU · 2 GB$18
DigitalOcean2 vCPU · 4 GB$24
DigitalOcean4 vCPU · 8 GB$48
AWS Lightsail (provisioner default)512 MB · 2 vCPU$5
AWS Lightsail1 GB · 2 vCPU$7
AWS Lightsail2 GB · 2 vCPU$12
Fly.io (usage-billed, from)shared-cpu-1x · 256 MBfrom $3
Fly.io (usage-billed, from)shared-cpu-1x · 1 GBfrom $6
A server you already have—whatever you already pay

We do not publish a team size any of these fits. No benchmark has been run, so the only honest number is the price.

Fly.io and Cloudflare appear here as places you could run a container by hand. The one-click provisioner does not target them — see Not yet supported.

3. Stand up the relay

The Docker path (this is what was run)

The Compose file below is the product's own generated recipe, not something written by hand. The test run imported the product's buildRelayComposeRecipe and ran its output unchanged except for renaming the container, the volume and the published port so it could be torn down cleanly.

In an empty directory on the relay host, devices.json — the allowlist. It must not be empty. Start with your own device; §4 shows where that line comes from.

{
  "244805373dc4e11fb92164033546a2db": "MCowBQYDK2VwAyEAPuhy+xCh1I9u5+RqySduhDTG43PuQ0dSILyVcDFslu4="
}

docker-compose.yml:

services:
  relay:
    image: node:22-slim
    container_name: excellent-relay
    restart: unless-stopped
    ports:
      - "${RELAY_PORT:-9700}:9700"
    environment:
      EXCELLENT_DATA_DIR: /data
    command: ["sh", "-c", "npm i -g @excellent-so/cli && exec excellent-mcp sync relay serve --host 0.0.0.0 --port 9700 --devices /etc/excellent/devices.json --log /data/relay-log.ndjson"]
    volumes:
      - relay-data:/data
      - ./devices.json:/etc/excellent/devices.json:ro
volumes:
  relay-data:
    name: excellent-relay-data
docker compose up -d
docker compose logs -f relay

Startup-line excerpt from the 2026-09-24 run, about 24 seconds after the container starts — the npm install runs first, and the relay is unavailable for that whole window on every restart:

relay listening on http://0.0.0.0:9700 (1 device(s); log /data/relay-log.ndjson)

The old output also told every node to run the owner's setup command. Those obsolete instruction lines are omitted here. Use a relay URL reachable from every Mac; 0.0.0.0 above is a listen address, not the address to give teammates.

The current CLI help gives this sequence for switching from single-owner to team sync: freeze writes, configure the owner, then join teammates with the owner's workspace bundle. Pair every device and add its public key to devices.json; retry pairing after the roster edit if the device was initially refused. Start the owner to seed the relay, then start Excellent on the other machines. Only the owner uses setup --mode team; teammates use setup --team-bundle. Follow §4 and §5 for the commands. This guidance was checked against current source; it is not additional terminal output captured in the dated run above.

The in-app provisioner

Excellent ships a one-click provisioner for the relay under Settings → Team. It generates exactly the Compose file above, seeds the roster with your own device, and polls until the relay answers. Its targets are Docker, DigitalOcean and AWS Lightsail; give it a domain and it also stands up Caddy for HTTPS.

Its success message tells you to add each teammate's device to devices.json on the relay host. The relay re-reads that file when it changes, so additions and revocations take effect without a restart. The manual roster edit is still required — see §5.

The provisioner's screens were read in the product source but not clicked through: walking them needs the full app running with an authenticated owner session, which this run did not do. So the screen-by-screen walkthrough is not published here. What is published is the Compose file it emits, which was generated by its own builder and run.

Running it without Docker

The container runs this command; on a host with Node 22 and the CLI installed it is the same command:

npm install -g @excellent-so/cli
excellent-mcp sync relay serve \
  --host 0.0.0.0 \
  --port 9700 \
  --devices /etc/excellent/devices.json \
  --log /var/lib/excellent/relay-log.ndjson
FlagMeaning
--hostBind address. 0.0.0.0 is what the generated recipe uses; a loopback bind is invisible to other machines.
--portListen port. The recipe's default is 9700.
--devices <file>The JSON allowlist, { "<deviceId>": "<publicKey>" }. Required, and must not be empty.
--log <file>The append-only change log.

Wrap the process in systemd, launchd or a supervisor so it survives a reboot. Only the containerised form was exercised in testing.

4. Point a Mac at the relay

The sync subcommands that exist, from the CLI's own error message:

$ excellent-mcp sync --help
unknown 'sync' subcommand: --help (try setup|join|adopt|status|now|pair|workspace-id|invite-team|relay serve)

If workspace-id and invite-team are missing from that list, your CLI predates the join fix. Upgrade (§9) before going further.

Which setup you run depends on which side you are on.

The first machine — the owner of the workspace everyone else will join:

excellent-mcp sync setup --mode team --relay http://<relay-host>:9700
excellent-mcp sync pair  --relay http://<relay-host>:9700

A teammate joining that workspace uses the bundle the owner sends them (§5) instead of --mode team:

excellent-mcp sync setup --team-bundle <blob>
excellent-mcp sync pair --relay http://<relay-host>:9700

Running --mode team on the second machine is the mistake that costs you an afternoon: it configures that node as the owner of its own workspace, and the relay will correctly send it nothing, forever. The sync loop now says so out loud after a few empty ticks, naming the command that fixes it.

sync setup writes sync.env in the data directory, mode 0600, holding EXCELLENT_SYNC_MODE='team' and EXCELLENT_RELAY_URL. Source it before starting Excellent. sync pair mints this machine's Ed25519 device key and prints the one line the relay operator needs:

$ excellent-mcp sync pair --relay http://<relay-host>:9700
device: 244805373dc4e11fb92164033546a2db
public key (give this to the relay operator / devices.json):
  "244805373dc4e11fb92164033546a2db": "MCowBQYDK2VwAyEAPuhy+xCh1I9u5+RqySduhDTG43PuQ0dSILyVcDFslu4="

Against a relay that has not been told about this device, pairing is refused:

pairing with http://<relay-host>:9700 failed: relay /pair/challenge → HTTP 404 {"ok":false,"reason":"unknown-device"}
Is this device provisioned on the relay? (sync relay serve --devices …)

That refusal is the allowlist doing its job.

This used to be refused outright. Until 2026-09-24, sync setup --mode team exited 2 with a solo-node identity message, because agent ids were derived from the process tree and were not unique across machines. They are now salted with a per-device fingerprint, and team sync is allowed. The remote-worker role is still refused, and for a reason that has not changed: it needs a distributed lock that does not exist.

The product's own runbook names the in-app equivalent as Settings → Team → Primary & sync → Team sync (conflict-free). That path was not walked in this run, so nothing about its screens is published here — and, as §5 says, it cannot join someone else's workspace in any case.

5. Adding a device to the roster

There is no roster API. The relay's complete route list is GET /health, POST /pair/challenge, POST /pair/verify, POST /register, POST /push, POST /pull, and — only when webhooks are enabled — POST /inbox/pull and the /hooks/<platform>/<deviceId> pair. Nothing else exists to call.

Onboarding a teammate is therefore two separate things, and both have to happen. Doing only one is the failure mode this section exists to prevent.

a. Send them the workspace, so their node knows what to join

On the machine that already has the workspace:

excellent-mcp sync invite-team

It prints one base64url line, then instructions. That line carries a relay URL and a workspace id and nothing else — it is not a credential, it grants no access, and on its own it gets a teammate nowhere. The command's own output says so. Send it however you would send an address.

excellent-mcp sync workspace-id prints just the workspace id on stdout, one line, if you would rather assemble things yourself.

The teammate runs, on their machine:

excellent-mcp sync setup --team-bundle <the line you sent>
excellent-mcp sync pair --relay http://<relay-host>:9700

setup --team-bundle points their node at your workspace instead of the one their install minted for itself, and it says so when it does:

aligned workspace 1 ('default') to the joined uid: 01A0D1DD84B0068570230B9458 -> 01A0D1DD7A3315DA4231358547

It refuses rather than guessing when the answer is not obvious — if their install has more than one live workspace, or if the local workspace already holds real work that would silently become yours. The refusal names the flag that overrides it.

b. Put their device on the roster, so the relay will talk to them

sync pair prints their device line. You paste it into devices.json on the relay host:

{
  "244805373dc4e11fb92164033546a2db": "MCowBQYDK2VwAyEAPuhy+xCh1I9u5+RqySduhDTG43PuQ0dSILyVcDFslu4=",
  "68540a33f46110da273cb3ea789b0ed2": "MCowBQYDK2VwAyEAj1Xcz0Lo56gMNS7Z9GcSZr5CGrsPLRy0Z/H3QXPmElc="
}

No restart. The relay re-reads that file whenever it changes, before it answers a pairing request and before it resolves any bearer token, so the addition lands on their next attempt. It logs what moved:

[relay] roster reloaded from /etc/excellent/devices.json: +1 device(s) (68540a33…), -0 REVOKED; 2 device(s) now allowed.

A roster file that is unreadable or does not parse keeps the last good roster and says so on stderr. It never falls back to allowing everyone, and it never empties itself and locks the team out.

Until their line is in the file, pairing is refused with 404 unknown-device — that refusal is the allowlist doing its job.

What a first join actually looks like

The joining node receives the workspace on its first tick. It will also record a handful of parked rows and a couple of conflicts, every time, and that is expected rather than a sign of damage: both installs seeded a project called general and an identical set of loop and rubric rows at first run, so those genuinely-different rows collide on the same unique keys. One copy of each loses and is surfaced at /database/conflicts instead of being silently merged. Nothing is lost. It is alarming to look at and it is not caused by the join.

About invitations

There is no invite link, and excellent-mcp sync join --invite still cannot join a team relay. That bundle belongs to the older single-owner sync mode: it carries a database URL and a login coupon and has no field for a relay address, so sync join refuses a bundle without a database URL and then writes a single-owner configuration.

sync invite-team above is a different thing despite the name. It hands over an address, not an authorisation, and the teammate is still refused by the relay until you have done step (b) by hand.

6. HTTPS and a domain

The relay speaks plain HTTP and has no TLS of its own. Device authentication is a signed Ed25519 challenge, but the contents of your workspace are readable on the wire. Plain HTTP is acceptable only on a trusted LAN or a VPN. Do not expose the relay directly to the internet.

For anything crossing the internet, terminate TLS in front of it. Giving the provisioner a domain does this for you: the relay stops publishing its own port and Caddy takes 80/443 with an automatic Let's Encrypt certificate. The generated service:

  caddy:
    image: caddy:2
    container_name: excellent-caddy
    restart: unless-stopped
    depends_on:
      - relay
    ports:
      - "80:80"
      - "443:443"
    command: caddy reverse-proxy --from relay.your-team.example --to relay:9700
    volumes:
      - caddy-data:/data
      - caddy-config:/config

The manual step is DNS: point the domain's A/AAAA record at the host's public IP. Until it resolves, the provisioner reports the relay as pending-dns rather than reachable. Teammates then use the https:// URL.

Not exercised. The Compose fragment above is what the product generates, and it was read from the source — but no certificate was ever issued during testing. Let's Encrypt's HTTP-01 challenge needs a public DNS name resolving to a publicly reachable host on port 80, which a laptop behind NAT cannot provide. Treat TLS as unverified until you have run it on a real host with a real domain.

7. Backups and restore

Your first line of defence is that every teammate's Mac already holds a complete copy of the workspace. Cold backups cover what redundancy does not: someone deletes something everywhere, disk loss, ransomware.

On the relay host, back up two things: the append-only change log, and devices.json.

With the Docker recipe the log lives in a named volume, not in a home directory, so a file-level backup of the host will miss it entirely:

docker cp excellent-relay:/data/relay-log.ndjson ./relay-log.ndjson
cp devices.json ./devices.json

The log only grows, so a daily snapshot gives you a clean replay point. Stop the relay or snapshot the volume while copying so you do not capture a half-written line.

On each Mac, back up the whole data directory — data.db, local.db, device-key.pem, sync.env, keys/. Do it before anything risky.

Read this before you rely on a Mac backup. data.db is encrypted at rest with SQLCipher and the key is not in the data directory — it lives in the machine's OS keychain. A copy of data.db alone cannot be opened on a different machine. Either restore onto the same machine with its keychain intact, or export and preserve the key alongside the backup.

Restoring a single Mac end to end, including the keychain step, was not walked in testing, so no procedure for it is published here.

Restoring the relay

Verified by destroying the container and its volume, then restoring from a copy of the log:

docker compose up -d
docker cp ./relay-log.ndjson excellent-relay:/data/relay-log.ndjson
docker compose restart relay

Real output after the restore — all 303 entries came back and devices reconnected:

relay listening on http://0.0.0.0:9700 (2 device(s); log /data/relay-log.ndjson)
$ docker exec excellent-relay wc -l /data/relay-log.ndjson
303 /data/relay-log.ndjson

The product's runbook says a brand-new device can replay the whole history from cursor 0. That was tested against this restored log with a clean third node: it received nothing, for the same reason as §5. Do not plan on a fresh device as a recovery path.

8. Revoking a device

Delete the device's line from devices.json. That is the whole procedure.

No restart. The relay re-reads the roster when the file changes, and a removal drops the device's key, its live session, its bearer tokens and its routing scope together — so a token issued a second earlier stops working too. This was tested on 2026-09-24 with the relay process untouched: the removed device was refused, it never received a change written after its removal, and the container's restart count stayed at zero.

pairing with http://<relay>:9700 failed: relay /pair/challenge → HTTP 404 {"ok":false,"reason":"unknown-device"}

On a Linux relay host this is immediate. The reload is triggered by the roster file's modification time as the relay process sees it. On the macOS Docker bind mount used for testing, that propagated about a second late — long enough that a /health snapshot taken half a second after the edit still counted the old device. A Linux host, which is the documented deployment, has no such layer.

The relay host's own device is a base entry that a file edit cannot remove, so you cannot lock yourself out of your own relay by editing the roster.

The revoked machine keeps its own local copy of everything it had already synced. Revocation stops future exchange; it does not reach back and delete.

9. Upgrading

Migrations are additive-only, so nodes on different builds can straddle a version. Take a backup first.

Each Mac:

npm install -g @excellent-so/cli@latest

The relay has no schema of its own — its state is the NDJSON log plus the roster file, and a newer version replays the log unchanged. Because the container installs the CLI at every start, restarting it already picks up the latest published version:

docker compose restart relay

Keep relay-log.ndjson and devices.json in place across the upgrade.

Pin the version in production. The generated command is npm i -g @excellent-so/cli with no version, which means the relay can change underneath you on an unrelated restart. The published version at the time of this run was 0.1.0 (checked 2026-09-23, and it is a point-in-time reading — run npm view @excellent-so/cli dist-tags for today's). Pin @excellent-so/cli@<version> in the Compose command once you have one you trust.

10. Is it up?

Check that the container is running, that the port answers, and that the log is growing.

docker compose ps
docker compose logs --tail 20 relay
curl -s http://<relay>:9700/health

GET /health answers with the roster size, the uptime and the head of the change log:

$ curl -s http://<relay>:9700/health
{"ok":true,"service":"excellent-relay","devices":2,"uptime":17,"head":135}

A rising head is the real signal that changes are flowing. devices is the roster as the relay currently reads it, which is how you confirm an addition or a revocation landed without restarting anything.

That endpoint is new as of 2026-09-24, and it names itself on purpose: the provisioner's "reachable" poll now requires ok: true and service: "excellent-relay" before it calls a relay up. It used to accept any status below 500, which is true of any HTTP server that happens to hold the port.

excellent-mcp sync status still cannot see team mode. It inspects only the older single-owner replica flag, and returns "state": "single-player" on a healthy, actively-syncing team node — and identically on a revoked one, and on a machine that was never configured. Do not use it to diagnose the relay; use /health and the node's own tick log.

11. Troubleshooting

Every error below was produced during testing.

ERR_DLOPEN_FAILED … NODE_MODULE_VERSION 127 … requires 137 You are on Node 24. The native modules are built for Node 22. Switch (nvm use 22) and retry.

sync setup: team-sync is refused: this node runs the SOLO-NODE identity implementation… Your CLI predates 2026-09-24. Agent ids are now salted with a per-device fingerprint and team sync is allowed; upgrade (§9). The same refusal on --role remote-worker is not a version problem and is still correct.

SqliteError: no such table: main.document_targets when team sync starts The database was created by the CLI alone and is missing tables only the app creates. The sync registry classifies those tables as team-shared, so enabling the merge shadow fails closed. Launch Excellent once on that machine first. The terminal-only onboarding path dead-ends here.

relay /pair/challenge → HTTP 404 {"ok":false,"reason":"unknown-device"} The device is not in devices.json, or the public key was truncated in transit, or the file no longer parses — a broken roster keeps the last good one and says so on the relay's stderr. Copy the whole "<deviceId>": "<publicKey>" line and check the relay's log for the roster reloaded line naming the device. No restart is needed to pick up the edit.

Everything reports healthy, and the other machine is empty That node is not joined to your workspace — it is the owner of its own. It happens when the second machine was set up with sync setup --mode team instead of sync setup --team-bundle, or from the in-app switch, which cannot join. The node now says so itself after a few empty ticks:

[team-sync] 5 ticks and this node has NEVER received a row from a peer. … if you meant to JOIN a teammate's workspace, this node is NOT configured to (EXCELLENT_TEAM_WORKSPACE_UID is unset), so it is registering a scope the relay can never match and will correctly send you nothing, forever.

Re-run sync setup --team-bundle <blob> on that machine (§5a). The sync cursors are reset automatically when the scope changes, once, so you do not have to touch team-sync-state.json — the warning fires only on a node that has never received anything, so a quiet, converged team does not trip it.

The joining machine filled up with conflicts on its first sync Expected, and not damage — see §5. Both installs seed a project called general and an identical loop and rubric set, so those rows collide on their unique keys and one copy of each is parked at /database/conflicts rather than silently merged.

A node was working, then stopped after a relay restart Relay sessions are in-memory. Every restart de-authenticates every device; the app's boot loop re-pairs. If it does not, that device is no longer on the roster.

The relay dies about 90 seconds after it starts Your CLI predates 2026-09-24, when sync relay serve was added to the long-running exemption list in the CLI's own watchdog. Before that the watchdog killed it — silently survivable inside Docker, where the relay is PID 1, and fatal under systemd or in a plain shell. Upgrade (§9).

A node cannot reach the relay at all In order: is the container up? Is it bound to 0.0.0.0 rather than loopback? Is the port open through the host firewall or cloud security group? Does the URL in sync.env match exactly, including scheme and port? For HTTPS, has the certificate actually issued — that is, does DNS point at the box?

The team-sync switch refuses on a machine That machine is not an Apple-Silicon Mac. The refusal reason is platform-unsupported. There is no safe override; the merge engine is not built for it.

Relay restarts are slow, or fail with an npm error Every start runs npm i -g @excellent-so/cli, which took about 24 seconds and needs the npm registry. If the registry is unreachable, the relay does not start. Pin the version (§9) and keep the host's outbound access open.

Not yet supported

Each of these was confirmed absent, not assumed.

  • Team-visible verification evidence. Attempts, gate-run receipts, DoD judgments and evidence blobs stay on the machine that produced them.
  • Joining from the app. The in-app team switch takes a relay address but no workspace id, so it can only ever make a node the owner of its own workspace. A teammate joins from the command line. See §5.
  • An invitation flow for the relay. sync invite-team hands over an address, not an authorisation, and the teammate is still added to the roster by hand. sync join --invite cannot join a team relay at all: it targets the older single-owner mode, and its bundle has no relay field.
  • A roster API. Adding or removing a device is a hand edit of devices.json on the relay host. The relay picks the edit up without a restart, but nothing makes the edit for you.
  • Monitoring. GET /health exists and tells you the roster size, uptime and log head, but nothing watches the relay for you and nothing will tell you it is down.
  • Team-aware sync status.
  • A prebuilt relay image. The relay reinstalls the CLI from npm on every start.
  • Windows, Intel or Linux editing machines. Apple Silicon only. The relay host itself can be Linux.
  • Fly.io and Cloudflare provisioning targets. The one-click provisioner covers Docker, DigitalOcean and AWS Lightsail.
  • Owner transfer, and a guard against removing the last owner.
  • Device-key rotation. Rotation exists for the older single-owner tokens. Relay device keys have no rotation path.
  • mTLS and certificate pinning. TLS terminated by your own reverse proxy is the model.
  • SSO, SAML, SCIM, directory sync, audit-log export.
  • A managed or Excellent-hosted relay, per-seat pricing, or enterprise plans. Every team self-hosts. There is nothing to buy here.
  • A long-running integrity soak or scheduled two-machine CI. Neither has been run, so this page publishes no uptime or durability claim.

When something is wrong

Send the relay's container status and last 20 log lines, and the current line count of relay-log.ndjson. Those three localize nearly everything in §11.

Where to go next

  • Security and data — what is stored locally, and what leaves the machine.
  • Receipts — what a receipt binds, and how to verify one offline.
  • CLI reference — every command the installed CLI actually has.