TinyBox Networkdeployment manual · 0.5.477

Deployment manual

Everything below is in the package. Nothing here talks to a service you do not run: the relay is one Node process, the machines are one binary each, and the only thing crossing the network between them is your own agent traffic.

1. What you need

The relayA Linux host with Node.js 20+, a DNS name, and a TLS terminator (nginx, Caddy). About 300 MB of disk with the binaries it hands out. It holds no agent credentials.
A donor machineLinux or Windows, with the coding agent (Claude Code, Codex) installed and signed in as its owner. This is the machine whose subscription gets lent.
A consumer machineLinux, macOS or Windows. Nothing installed — one command does it.

2. Unpack and configure

tar xzf tinybox-network-0.5.477-swallowed.tar.gz
cd tinybox-network-0.5.477-swallowed
cp config.example.json config.json

Edit config.json. The five fields that matter:

instance.idShort id for this node, e.g. node1. It appears in the console and in federation.
domains.nodeThe public name this node answers on, e.g. tinybox.example.com. No domain is hardcoded anywhere else — this is the only place one appears.
server.portThe loopback port the app listens on (default 5320). Your TLS terminator proxies to it.
server.dataDirWhere accounts, sessions and the usage ledger live. Outside the release directory on purpose, so an upgrade can never take it with it.
access.invitationCodeRegistration is by invitation. Change this.

3. Run it

cd server && npm ci --omit=dev
AMERC_CONFIG=../config.json AMERC_PORT=5320 \
  AMERC_DATA_DIR=/var/lib/tinybox-network node app.js

As a service, the package ships deploy/amerc.service.template; fill in the paths and drop it in /etc/systemd/system/. In front of it, any reverse proxy that forwards websockets:

location / {
    proxy_pass http://127.0.0.1:5320;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;   # "upgrade" when $http_upgrade is set
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_read_timeout 3600s;
}
The websocket upgrade is not optional. A donor and a consumer both hold one open, and the terminal rides it. A proxy that drops Upgrade gives you a console that loads and a relay that never connects.

The package's deploy/deploy.py does all of the above over ssh from a deploy/nodes.json if you would rather describe the boxes than log into them.

4. First administrator — there is no default password

A fresh node ships no accounts at all. Nothing is pre-seeded, so there is no default login to look up and none to forget to change.

Open the node and go to /setup. It is open exactly once: you choose the first administrator's username and password there, and the moment that account exists /setup stops answering and every other page redirects to sign-in.

Do this before you point DNS at it. Between the first start and your visit to /setup, whoever reaches the port first becomes the administrator. On a box that is already public, start the service bound to loopback, run /setup through an SSH tunnel, and only then open the proxy.

That first account is also the node's first donor and consumer — it is given the whole pool, so you can put your own machine on it immediately without editing anything.

Everyone after you registers at /register with an invitation code. The one in access.invitationCode is what a new person types; an administrator can also issue single-use codes from Administration → Accounts & access → Invitations, each carrying the rights and shards that account should start with.

Forgot the passwordThere is no recovery link. Another administrator can set it from Accounts & access (Set password / Reset password). If it is the only administrator, stop the service and edit status/passwordHash in <dataDir>/users/<name>.json — it is plain JSON on purpose.
Locked out entirelyMove <dataDir>/users/ aside and restart: with no users, /setup answers again. Sessions, donors and the usage ledger are separate files and survive.

5. Add machines

Sign in, open TinyBox → Donor (or Consumer), copy the line. The address carries that account's token, so nothing is typed on the machine itself:

# the machine that LENDS its agent
curl -fsSL 'https://tinybox.example.com/donor.sh?k=<token>&config=claude' | sh
irm 'https://tinybox.example.com/donor.ps1?k=<token>&config=claude' | iex

It installs into ~/.local/share/amerc/donor-<agent> (or %LOCALAPPDATA%\amerc\donor-<agent>), starts the supervisor, and prints the pid, the folder, the log and how to stop it. The supervisor keeps the binary up to date from your node — no cron job, no scheduled task.

One donor program per agent per machine, per network. Two copies pointed at the same relay replace each other's connection every few seconds, forever: sessions never start, and from the machine both copies look healthy. The installer refuses to become the second one. The same machine may lend the same agent to two separate networks — those donors never meet.

6. Watch it work

Administration → TinyBox relay network → Donors. Each machine reads OK, or BAD with the one reason that stops it — not answering, not signed in, renewing its login, out of quota, or its agent will not start. Quota is every window the seat publishes, not just the worst one.

Sessions shows who is attached to whom, with per-leg round trips. Usage is what each account has run up, historical and ongoing. Binaries is every build this node serves and which channel it is on.

7. Form a network

Give every node the same federation.serverKey (64 hex characters; any one of them can generate it) and list them all in federation.peers:

"federation": {
  "enabled": true,
  "serverKey": "<the same 64 hex characters on every node>",
  "bridge": { "enabled": true, "host": "0.0.0.0", "port": 30453 },
  "peers": [
    { "id": "node1", "url": "https://tinybox.example.com",
      "bridge": { "host": "203.0.113.10", "port": 30453, "dialFrom": ["node2"] } },
    { "id": "node2", "url": "https://tinybox.example.cn",
      "bridge": { "host": "198.51.100.20", "port": 30453, "dialFrom": [] } }
  ]
}

dialFrom says which peers dial this one, so a node behind NAT can list nobody and still be reached — the other side dials it. Accounts, invitations and donor rosters are then shared: a machine reaches the node nearest to it and can still be served by capacity donated on the other side of the world.

8. When something is wrong

The console loads, no donor appearsThe websocket upgrade is not getting through the proxy, or the account has no pool. Administration → Logs & requests shows the request ring: a 403 on /ws/relay is a pool that does not include that agent.
A donor says BADThe row carries the reason. "not signed in" means exactly that: sign in to the agent on that machine, as its owner. The relay cannot and will not do it for you.
A donor keeps reconnectingTwo donor programs for the same agent. Stop the extra one; the row says so once it has happened twice in two minutes.
Sessions die mid-conversationThe donor's login expired. The row shows how long each login has left, and a donor renews its own if it can.

What this does not do

It does not hold your provider credentials, proxy your model API, or see inside a session beyond what it must relay. A donor lends its own login, in place, on its own machine; a session can refresh it (which is how it stays alive at all), and a session that signs it out signs it out. That is a trust boundary, not a file permission, and it belongs with who you lend to.