partial 10 min verified 503cd17 (2026-09-13)
Publish a service on the mesh
Publishing gives your app a <name>.mesh address that resolves from every
router, and lists it in the Services panel on every hello.mesh. There are
two ways in.
| Lane | Who | Status |
|---|---|---|
| Operator publish | Someone with SSH to a router | Built |
| Key-owned name | The app itself, signing with its own key | Built on the router side. No reference client library yet (bead 8tk), and signatures aren't re-checked mesh-wide yet |
Lane 1: Operator publish
The publish command talks to the router's local control API, which only
listens on 127.0.0.1. So you run it on the router, over SSH.
The on-router command is mjolnir-meshd. Older docs say meshd publish,
and no meshd alias exists.
Step 1: Pick the right router
Do: SSH to the router whose LAN your app's host is plugged into:
ssh root@10.254.x.y.
A plain publish always uses that router's own gateway address
(10.42.x.1). A service on another box on that LAN needs --ip
(step 2b).
Step 2a: Publish a service the router itself serves
Do:
mjolnir-meshd publish keyed --port 3000
Expect: published keyed.mesh ip=10.42.x.1 port=3000.
Step 2b: Publish a device on the router's LAN
Do:
mjolnir-meshd publish keyed --ip 10.42.7.42 --port 3000 --mac aa:bb:cc:dd:ee:ff
Expect: a router-scoped name,
published keyed.<4-char>.mesh ip=10.42.7.42 port=3000. Share that full
name. The router-specific part stops two routers' devices from colliding.
Step 3: Add details (optional)
Do: add repeatable --txt key/value pairs, for example
--txt path=/app --txt proto=http.
For a web app, include --txt proto=http (or proto=https). That's what
makes hello.mesh show it as a clickable link; without it the entry shows as a
bare address.
Step 4: Check it from anywhere on the mesh
Do: from any device on any router's Wi-Fi, open http://hello.mesh and
look under Services. Or run nslookup keyed.mesh.
Expect: the name resolves to the published address, and a web service
(http/https) shows as a clickable link.
If not:
409conflict: another router already owns that name. The first publisher wins, and only the owner can refresh it. Pick another name.- Reserved names are rejected:
hello,id. - Not listed yet: the directory refreshes about every 5 seconds. Gossip can take a few more.
Unpublish
mjolnir-meshd unpublish keyed # plain publish
mjolnir-meshd unpublish keyed --device # a --ip device publish
Lane 2: Key-owned names (for app developers)
An app can hold a name with its own Ed25519 key, with no operator and no SSH. The name is a lease: it keeps resolving while the app renews it, and frees up after the app goes quiet.
| Rule | Value |
|---|---|
| Names per key | One |
| Stops resolving after | 90 s without renewal |
| Ownership lease | 1 hour. Another key can only take an expired lease |
| Signed message | mjolnir-name-claim:v1\n<challenge>\n<name>\n<port> |
Step 1: Get a challenge
Do: GET http://<any-router-LAN-gateway>/api/challenge
Expect: {"challenge":"<hex>"}, single-use and valid for 5 minutes.
Step 2: Sign
Do: sign the exact bytes
"mjolnir-name-claim:v1\n" + challenge + "\n" + name + "\n" + port with your
Ed25519 key. name must already be one lowercase DNS label that isn't
reserved. The router checks the bytes you signed, so don't let it
normalize anything. Use port 0 for a plain address record.
Step 3: Claim
Do:
POST /api/name-claim
Content-Type: application/json
{"pubkey":"<64 hex>","sig":"<128 hex>","challenge":"<hex>","name":"keyed","port":3000,"ip":"10.42.7.42","scheme":"https"}
ip and scheme are optional and aren't covered by the signature. Without
ip, the router uses your request's source address. Set scheme to http
or https so hello.mesh lists your name as a clickable link; without it the
name shows as a bare address.
Expect: accepted. Within about 5 seconds the router publishes the name, and it resolves mesh-wide.
Step 4: Renew
Do: repeat steps 1–3 with a fresh challenge well inside 90 seconds.
A working example of this lane is the walkie-talkie app's republish loop
(src/lib/server/mesh.ts in that app).
Current limits: only the router that ingests a claim checks its signature. Mesh-wide re-verification isn't built yet. Treat key-owned names as convenient, not as proof of identity.
Read the directory from your app
GET http://<any-router-LAN-gateway>/api/directory returns the mesh-wide
snapshot: node, neighbors, identities[], services[]. Every GET /api/*
sends Access-Control-Allow-Origin: *. There's no push yet, so poll about
every 5 seconds.
Full protocol reference: `docs/deploy/mesh-app-publishing.md`.
Next: Mini-apps (coming soon).