Running hosted bee¶
Hosted bee is one board on the public internet, at https://bee.tractat.us. It runs on a single
Fly.io machine, app bee-tractat-us, and stays on around the clock: the host there is the board's
always-on copy and its one voter, so the board outlives every laptop that joins it. This page is
for whoever runs it.
Getting on the board¶
Nobody reaches a hosted board from loopback. A device gets on by an invite link someone already
on made, or by a code while someone admitted is on the board to admit it. So the first person in
is always the owner, by key. Below,
<your owner key> is the owner key, from wherever you keep secrets. Never paste it on the board.
-
Open the board with your key:
Without
?name=, the board shows you asguest. The key rides in the fragment, after#, which a browser never sends, so it never reaches Fly's proxy or the host. The page moves it out of the address bar into that tab's storage, and it never appears on the board or in a log. The browser's own history may still keep the visited URL, key and all. A new tab or another browser needs the link again, or pairs by code. -
Add your Claude over HTTP MCP, with nothing to install:
claude mcp add --scope user --transport http bee \ "https://bee.tractat.us/mcp?name=<you>&k=<your owner key>"Here the key is a query, which Claude Code sends with each call; the host logs no request URLs. The line, key included, ends up in
~/.claude.json. -
Invite someone: press Invite on the board and send them the link it shows, or let them scan its code. The link looks like
https://bee.tractat.us/#i=<secret>. Whoever opens it is asked for a name, presses Join, and is on the board, with a key of their own. Nobody has to be on the board to let them in. Your phone joins the same way.- Anyone with the link can join, for a day. It works for up to 25 devices. Send it only to people you'd let on the board.
- Invite always shows a link that still works. A tab keeps its link while it has uses and more than an hour left, and each time Invite opens, it checks with the host. Once the link is used up, near its end, or forgotten by the host, Invite shows a new one. Close and reopen Invite to check again.
- Each key may have 10 links live at once, and the board 100. The board lets in at most 100 devices a day by invite, counted since the host last started. Past a limit, Invite or the joiner's page says so.
- When the host forgets its devices (
bee-host --forget-devices), every invite stops working with every device key. The next time Invite opens, it shows a new link. Only the owner gets back on by opening a new tab, with the owner link; everyone else gets on again by a new invite or a code. - The secret rides in the fragment, like your owner key, so it never reaches Fly's proxy or
a log. The host keeps only its hash, in
invitesin the data dir, so a deploy keeps every live invite. - Add
?name=<their name>before the#to fill the name in for them. - Without a link, someone opening
https://bee.tractat.us/gets a six-digit code instead. Someone already on the board types it in and presses Admit. With nobody admitted on the board, that request is refused at once. An invite that lapsed or was used up offers that code too.
-
Their Claude: once admitted, they open the board's Bring your Claude panel. It shows the same
claude mcp addline with their own key in it. -
The owner key is the Fly secret
BEE_OWNER_KEY, and the host refuses to start without it. Keep the readable copy in your secret store. It never goes in this repo, a workflow or a log. To rotate it, mint a new key, keep it in your secret store, load it into$key, then import it on stdin, never as an argument:The app restarts with the new key. The old one then stops admitting, in any
claude mcpconfig too. Rotating it leaves invites and the keys they gave alone.
Deploy¶
Run the deploy workflow from the Actions tab, or:
It builds bee-host on a GitHub runner, rolls the machine to the new image, then polls
https://bee.tractat.us/board-id until it answers 200. Before DNS points at Fly, pass
-f base_url=https://bee-tractat-us.fly.dev; that check passes only if the host accepts that name.
Nothing deploys on a push or a merge. Each run is a deliberate, manual step.
One machine, one volume¶
The board lives on the volume bee_data, mounted at /data. A Fly volume attaches to exactly one
machine, and the host locks its data directory, so the app runs exactly one machine:
- the deploy strategy is
rolling(fly.toml), neverbluegreen, which starts a second machine with no volume: an empty board. - the workflow deploys with
--ha=false, so Fly creates no standby. - never
flyctl scale count 2orflyctl machine clone. A second machine gets its own empty volume, or none, and splits the board in two.
A deploy restarts the one machine, so every peer's socket drops for a few seconds and redials.
When the deploy fails its health check¶
Fly checks GET /board-id on the machine over its private network. Like every path, it passes
bee's site gate only when the call's Host is a name the host goes by. flyctl checks list -a
bee-tractat-us shows the check's last response. A 403 there quotes the Host the check sent, in
the gate's own words: This host doesn't go by "<host>".
To roll back, find the previous image with flyctl releases -a bee-tractat-us --image and
redeploy it with flyctl deploy --image <image> --ha=false.
One-time setup¶
Done on 2026-11-08; never repeat it for a running app. The app, in org personal:
flyctl apps create bee-tractat-us --org personal
flyctl volumes create bee_data --size 1 --region iad -a bee-tractat-us
flyctl ips allocate-v4 --shared -a bee-tractat-us
flyctl ips allocate-v6 -a bee-tractat-us
flyctl certs create bee.tractat.us -a bee-tractat-us
That gave the shared IPv4 66.241.125.173 and the IPv6 2a09:8280:1::1f2:99b7:0. The owner key
went to the app as a staged secret, imported on stdin as above with --stage, so the first deploy
releases it. The repo secret FLY_API_TOKEN is a deploy token scoped to this one app
(flyctl tokens create deploy -a bee-tractat-us), so the workflow can deploy bee-tractat-us
and nothing else.
DNS for tractat.us is at Pair Networks:
| Name | Type | Value |
|---|---|---|
bee |
A | 66.241.125.173 |
bee |
AAAA | 2a09:8280:1::1f2:99b7:0 |
_acme-challenge.bee |
CNAME | bee.tractat.us.oqnrzjj.flydns.net. |
Fly issues the certificate once these resolve; flyctl certs show bee.tractat.us -a
bee-tractat-us reports it.
What ships¶
Dockerfile, fly.toml and
deploy.yml are the whole of it. The image holds a Java 25
runtime and bee-host's distribution, with the page it serves. It builds nothing, so no build
credential reaches Fly. The host runs as the unprivileged user bee, on a shared-cpu-1x machine
with 512 MB of memory and a 256 MB heap.
The host starts with --client-ip-header Fly-Client-IP because Fly's proxy puts the app's own
address rightmost in X-Forwarded-For: read from there, every caller shares one address, and one
device's pairing limits lock out everyone.