Phones talk to each other over Bluetooth and Wi-Fi with no server in between. When there is internet, a relay you run yourself — this one — carries the rest. Rooms are constellations drawn from the people in them; every message shows how it reached you.
In the app: you → settings → network → relays → add, paste the address, then tap ★ to make it your home — it keeps the history of every room you're in.
wss://beacon.proxdon.au/mesh
identity 0Hn0RArZVgEgi1RbzqjM1CmGG9ZCmPRMOAX7Gy7/S/o= — the app pins it on first sight and warns if it ever changes.
On first launch the phone makes an Ed25519 keypair; the public key is you, and a 24-word phrase encodes it. Write it down — it is the only backup. Every frame is signed. Other phones pin your key the first time they see it; if it ever changes, they say so and ask you to compare safety numbers in person.
Google Nearby Connections over Bluetooth, BLE and Wi-Fi — a flooding mesh. Every phone forwards what it hears, dedups by id, and decrements a TTL that starts at 6. That arithmetic is where hop counts come from. Voice is IMA-ADPCM at 16 kHz.
A WebSocket to a relay you choose — several at once if you like; a phone on two beacons bridges them. The beacon is deliberately dumb: it fans frames out, keeps history for anchored rooms, and lists public ones. It never sees a key that could decrypt a sealed room or a DM, and it stores no DMs at all.
A room is one conversation: one text stream, one voice room. It has an owner, mods, an access tier, post policy, slow mode, retention. Anchor it on a beacon to outlive your phone; make it local-only and no relay will ever carry a frame for it.
An ordinary message with drop and expires at the envelope level, so carriers see them without opening a sealed body. Phones store the raw signed bytes and re-offer them on every new mesh link. Never sent to a beacon. 7 days, 16 KB, 200 per phone; expired ones are refused.
Mark a relay ★ home and it keeps the history of every room you're in — 20,000 messages a room, on your box. Search reaches into it; a zip export is one tap. Two homes is failover for free.
The app holds its own connection — the honest cost of having no push server, and the reason there's a (hideable) "connected" notification. Messages land in Android's Conversations, can be bubbled, and take an inline reply from the shade.
Everything stays on the phone: history, media within a budget you set, keys, settings. Cloud backup is off on purpose — an app that promises sealed rooms should not be uploading room keys to Google. Export any conversation as text.
| tier | hides | doesn't hide |
|---|---|---|
| open | nothing | — |
| invite-only | keeps strangers out of the room | traffic still floods the mesh; a relay can read it |
| sealed | message content — AES-256-GCM, key from a password | that the room exists, who's in it, when they talk |
| direct | content — end-to-end, X25519 keys from each phone's identity | that two identities talked, and when |
It needs nothing else. This page, the app's fonts, the QR codes, the in-app update feed, the dashboard and the doctor are all inside it.
cd beacon
docker compose up -d
docker compose logs -f beacon # prints the public key on bootState in ./data/. Port 8787.
cd beacon
CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o beacon-linux-amd64 .
sudo ./deploy.sh ./beacon-linux-amd64
sudo systemctl restart beacon # deploy.sh doesn't restart a running oneA beacon user, /opt/beacon, a hardened unit, state in /var/lib/beacon/.
# Caddyfile
beacon.example.org {
reverse_proxy localhost:8787
}Then phones use wss://beacon.example.org/mesh. Start the beacon with -public wss://…/mesh so this page and /beacon.json advertise it.
beacon doctor -db /var/lib/beacon/beacon.db
Checks the service, config, disk, live stats, and the public path — DNS, certificate expiry, HTTP through the proxy, a real WebSocket ping through the proxy, identity match — then prints the URL and a QR. Exit 1 on any problem; put it in a cron.
| flag | default | meaning |
|---|---|---|
-addr | :8787 | listen address |
-path | /mesh | WebSocket path |
-db | beacon.db | bbolt file for anchored rooms, mirrors and history; beacon.json lives beside it |
-public | — | the URL phones use (wss://host/mesh); saved and advertised |
-apkdir | — | directory of asterism-vNNN.apk builds to offer as in-app updates |
-peer | — | another beacon's ws URL to federate with (repeatable): live traffic and directory both ways |
-mirrorkeep | 20000 | messages kept per room for people who call this beacon home |
-maxpeers · -rate · -burst | 512 · 250 · 500 | concurrent clients; per-client messages/sec and burst (voice is ~50/s) |
-dedup · -reannounce | 200000 · 15s | seen-id cache; how often anchored rooms are re-broadcast |
| path | what |
|---|---|
/mesh | the WebSocket phones connect to |
/ · /get | this page |
/dashboard | operator view: phones, rooms, stored bytes, drops, uptime |
/healthz · /stats · /nodes · /beacon.json | ok · JSON counters · the public room directory · identity, name, MOTD, policy, public URL, peers |
/app/latest.json · /app/<build>.apk | the in-app update feed |
/export/<room>.zip | a room's stored history: one jsonl per channel and a transcript |
/j/<room> | a join page for invite links |
Can: which identities connect, which rooms they're in, message sizes and timing, the content of open and invite-only rooms. Cannot: the content of sealed rooms or direct messages — and it stores no DMs at all. There is no account database because there are no accounts. Every privileged frame must be signed by a key the beacon has pinned; the operator's own key is the only admin credential.