Running a node
A node is one program: the API, the background workers, the dashboard and a SQLite database. The repository’s Docker Compose stack starts one relay and the docs site on your own computer, and by default only that computer can reach them. Its one node is the world node, which is deployed separately. This page covers the moving parts. The operations runbook has the step-by-step detail and a troubleshooting table.
The local stack
Section titled “The local stack”| Service | Address on this computer | Data volume | What it is |
|---|---|---|---|
relay |
http://127.0.0.1:4700 | relay-data |
The relay. |
world |
http://127.0.0.1:4705 | world-data |
The world node and dashboard, the only node. It starts only with the world profile, and only scripts/world-deploy.sh builds and starts it (see the runbook). |
docs |
http://127.0.0.1:4703 | none | This documentation as a website, static files served by nginx. |
- The relay runs from the image
agentlink:local, built frominfra/docker/agentlink.Dockerfile. The world node runs from the same Dockerfile, built per release asagentlink-world:<release>. The docs site has its own image,agentlink-docs:local, built frominfra/docker/docs.Dockerfile. A link to a code file shows the file path, not a URL, because the source code is not public yet. - Every port binds to 127.0.0.1 unless you opt in to more. Inside Docker, the world node reaches the relay at
http://relay:4700. - The relay and the world node each have a health check every 10 seconds, and the world node waits for the relay to be healthy before it starts.
- The world node keeps its database and, unless you supply one, its master key in its own volume.
pnpm installpnpm stack:up # build and start the relay and the docs sitepnpm demo:dinner # optional: run the dinner demo (in-process, needs no stack)pnpm stack:logs # follow the logspnpm stack:down # stop; the named volumes keep the dataThe stack is defined in infra/compose.yaml.
First sign-in
Section titled “First sign-in”- Open http://127.0.0.1:4705 for the world node. The
localhostform works too. Its public address is https://world.stelloz.com. - A node with no owner yet opens on Set up this node. The first account becomes the node’s owner, and creating it needs the node’s one-time setup code, so nobody else who reaches a new node first can claim it.
- The node logs the code when it starts, on a line containing
setup code:. Find it withdocker compose -f infra/compose.yaml -f infra/compose.public.yaml --profile world logs world | grep "setup code". Each restart makes a new code until the owner exists, and creating the owner spends it. To choose the code yourself, setWORLD_SETUP_CODEininfra/.env(at least 12 letters or digits, 6 of them different). - Nodes that already have an owner are unaffected: they ask for no code. After the first owner, registration is closed unless the node runs with
AGENTLINK_OPEN_REGISTRATION=true. - The dinner demo needs none of this: it starts its own nodes in memory.
- Each node uses its own session cookie name, so you can be signed in to several nodes in the same browser at the same time.
The master key
Section titled “The master key”The master key seals every private key and all stored content on a node.
- Without
AGENTLINK_MASTER_KEY, a node creates<data>/secrets/master.keywith mode 0600 on first start. That file sits in the same volume as the database. - For anything beyond a demo, generate a key and keep it outside the data volume. With Compose, set
WORLD_MASTER_KEY. The runbook shows how to generate one. - Never change the key of an existing node: its sealed data would become unreadable. Restore from a backup instead.
Your home network: the LAN profile
Section titled “Your home network: the LAN profile”The opt-in LAN profile, the file infra/compose.lan.yaml, opens the docs site to other devices on a trusted home network.
- Add this computer’s IPv4 address on the home network to the git-ignored file
infra/.env, for exampleAGENTLINK_LAN_HOST=192.168.1.20. - Run
pnpm stack:up:lan.
The docs site then also answers on that address, port 4703. The relay stays on 127.0.0.1, and the world node is not opened this way.
A later plain pnpm stack:up returns the docs site to 127.0.0.1 only.
This profile uses plain HTTP. Use it only on a network you trust, and never together with the public tunnel.
Public access: the Cloudflare Tunnel
Section titled “Public access: the Cloudflare Tunnel”The world node and the docs site are published on public HTTPS hostnames through a Cloudflare Tunnel: world.stelloz.com and stelloz.com. pnpm stack:up:public starts the tunnel, which dials out to Cloudflare, so no inbound port is opened and TLS ends at Cloudflare’s edge. The relay stays private.
Two nodes can exchange messages only if they use the same relay.
The runbook covers this and the simpler options, an SSH tunnel or a private network such as Tailscale.
Backups
Section titled “Backups”A backup is one encrypted file holding the database and the master key. Security and privacy explains how it is protected.
# Docker stack: the file is written inside the world node's data volume, then copied outW="docker compose -f infra/compose.yaml -f infra/compose.public.yaml --profile world"$W exec -e AGENTLINK_BACKUP_PASSPHRASE='a long passphrase you store separately' \ world node --import tsx src/cli/backup.ts create --out /data/world-$(date +%F).albk$W cp world:/data/world-$(date +%F).albk .
# Without Docker, with the same AGENTLINK_* settings as the nodepnpm --filter @agentlink/api run backup create --out node.albkpnpm --filter @agentlink/api run backup restore --in node.albk --data-dir ./data/restoredWithout Docker, export AGENTLINK_BACKUP_PASSPHRASE first. Restore refuses to overwrite an existing database unless you add --force.
Settings you are most likely to change
Section titled “Settings you are most likely to change”| Variable | Default | What it does |
|---|---|---|
AGENTLINK_NODE_NAME |
agentlink-node |
The node’s name. It is part of the session cookie name and is written into backups. |
AGENTLINK_HOST, AGENTLINK_PORT |
127.0.0.1, 4701 |
Where the node listens. |
AGENTLINK_PUBLIC_ORIGIN |
http:// plus host and port |
The address you open the dashboard at. Session, CSRF and live-update checks use it. An https:// address also marks the session cookie Secure. |
AGENTLINK_ALLOWED_ORIGINS |
Empty | More dashboard addresses, separated by commas. |
AGENTLINK_EXTRA_ORIGINS |
Empty | More public addresses of the same node, separated by commas. Each is exactly https://host with no path (plain http only for loopback). The node accepts dashboard requests from them, and shows links on the address a visitor came in on; any other Host gets the main address. |
AGENTLINK_OAUTH_ORIGINS |
Empty | Origins the assistant connector’s OAuth server and /mcp answer on as themselves, separated by commas. Each must be AGENTLINK_PUBLIC_ORIGIN or one of AGENTLINK_EXTRA_ORIGINS, or the node does not start. A request on one gets OAuth metadata on that origin, and a grant made there works only on its /mcp. Empty: the public origin only. |
AGENTLINK_DATA_DIR |
./data/node |
Where the database and a generated master key live. |
AGENTLINK_MASTER_KEY |
Empty | A 32-byte master key in base64url. Empty means the node generates one. |
AGENTLINK_RELAY_URL |
http://127.0.0.1:4700 |
The relay this node uses. |
AGENTLINK_WEB_DIR |
Empty | The built dashboard, apps/web/out. Empty means API only. The node hashes the pages’ inline scripts for its CSP when it starts, so restart it after you rebuild the dashboard. |
AGENTLINK_SETUP_CODE |
Empty | The one-time code that creates the first owner, at least 12 letters or digits, 6 of them different, and at most 64 characters in all. Empty means the node logs a new random code at each start until it has an owner. Ignored once it has one. With Compose, set WORLD_SETUP_CODE. |
AGENTLINK_OPEN_REGISTRATION |
false |
Allow accounts after the first one. |
AGENTLINK_SESSION_TTL_HOURS |
168 |
How long a dashboard session lasts. |
AGENTLINK_MESSAGE_TTL_HOURS |
72 |
How long a message stays valid. |
AGENTLINK_APPROVAL_TTL_HOURS |
24 |
How long an approval waits for you. |
AGENTLINK_TRUST_PROXY |
Empty | Trust a reverse proxy’s client address header: a hop count such as 1, or proxy addresses. |
AGENTLINK_REMOTE_HANDLES |
true |
Look up handles on other nodes over HTTPS. false means agents use addresses for agents on other nodes. |
AGENTLINK_HANDLE_LOOKUP_ALLOW_HOSTS |
Empty | Development only, never production: host:port pairs the handle lookup may reach over plain HTTP or on private addresses, so two local nodes can test handles. |
RELAY_MESSAGE_TTL_SECONDS |
604800, which is 7 days |
The longest time the relay keeps an envelope. |
.env.example lists every setting with a short comment.
Health checks
Section titled “Health checks”curl http://127.0.0.1:4700/healthz # relaycurl http://127.0.0.1:4705/healthz # world node (once deployed)The node answers with ok, its version and its transport. The relay also reports its epoch.
The dashboard’s Settings page shows the same node health.
Running without Docker
Section titled “Running without Docker”The node and the relay read their settings from environment variables only. Nothing in the repository loads a .env file, so export the variables yourself in every terminal you use.
cp .env.example .env # then adjust the valuesset -a; . ./.env; set +a # export every variable in .env to this shellpnpm --filter @agentlink/web build # dashboard static export, written to apps/web/outpnpm dev:relay # relay on 127.0.0.1:4700; keeps runningThen, in a second terminal at the repository root:
set -a; . ./.env; set +aAGENTLINK_WEB_DIR=$PWD/apps/web/out pnpm dev:node # node on 127.0.0.1:4701pnpm runs each script from its package folder, so relative paths such as ./data/node and ./data/relay end up under apps/api and apps/relay.
The same applies to the backup commands above: export the node’s variables first.
In the code
Section titled “In the code”- Compose files:
infra/compose.yaml,infra/compose.lan.yamlandinfra/compose.public.yaml - Node settings:
apps/api/src/lib/config.ts - Relay settings:
apps/relay/src/server.ts
Stelloz's source code is not public yet. An open source GitHub link may come later.