No servers. No internet. No cell towers. No accounts. Devices on any shared local network find each other and relay end-to-end-signed messages — spreading them peer to peer, and carrying them across nodes that are never online at the same time.
Quickstart · See it converge · How it works · Security model · Protocol · Threat model
When an earthquake, flood, storm, or blackout takes out the network, communication fails at exactly the moment it matters most. The same is true under censorship and across the vast areas of the world with no reliable connectivity. Centralized messaging has a single point of failure: the infrastructure.
Starling removes the infrastructure. Every device is a peer. Messages hop directly from phone to laptop to radio-bridged node and propagate like a murmuration — the way a flock of starlings turns as one with no leader. A message you send underground reaches the surface the moment any node you've met later meets a node that hasn't seen it. It's the architecture behind Briar, Meshtastic and Bridgefy — distilled into a clean, auditable, zero-dependency library, a CLI, a live dashboard, and a runnable simulator.
Built from the same techniques serious distributed systems and secure messengers use — each implemented from scratch on Node's standard library, and covered by tests:
| Technique | What it gives you | |
|---|---|---|
| 🦠 | Epidemic gossip + anti-entropy | Eventually-consistent, delay-tolerant, store-and-forward delivery across nodes never online together |
| 🧬 | CRDTs (state- & op-based) | Conflict-free shared state — edits made while partitioned merge automatically, no coordinator |
| 🔐 | Ed25519 + X25519 + ChaCha20-Poly1305 | Signed, content-addressed messages; optional encrypted channels and forward-secret direct messages |
| 🌸 | Bloom-filter digests | Fixed-size, no-false-negative "what I hold" summaries to detect divergence cheaply |
| ⛏️ | Proof-of-work (hashcash) | Optional per-message cost that makes flooding expensive — no central gatekeeper |
| 🪶 | Zero dependencies | Just crypto, dgram, http, events. No build step. Installs and audits in seconds |
npm install -g starling-mesh # or run from a clone — there is nothing to buildstarling simulate # watch a whole mesh converge in one process (no network needed)
starling chat --name kit # real chat over your LAN/hotspot — the internet can be OFF
starling serve # a node + live web dashboard at http://localhost:8730Encrypted channels and direct messages are one flag / one call away:
starling chat --channel ops --secret "correct horse battery staple" # 🔒 end-to-end encrypted channelnode.dm(bobPeerId, 'the package is under the bench'); // 🔒 sealed to Bob's key alone (forward secrecy)Testing two nodes on one machine? Give the second a throwaway identity:
starling chat --ephemeral.
No hardware, no setup — simulate an entire lossy, partitioned mesh in one process:
$ starling simulate --nodes 12 --loss 0.1
✦ Starling mesh simulation
12 nodes · 10% packet loss · ~12ms links · ring+chord topology
1. Broadcasting from random nodes — epidemic spread across the mesh:
spread [████████████████████████████] 100%
→ all nodes hold all 6 messages in 81 ms
2. Network splits: island A (6 nodes) ⇿ island B (6 nodes) — link severed.
While split, island A holds 7/8 messages (B's news has not crossed: correct, blocked).
3. Partition heals — anti-entropy reconciles both islands:
reconcile [████████████████████████████] 100%
→ all nodes hold all 8 messages in 81 ms
─────────────────────────────────────────────
Result: ✓ every node converged to an identical message set
12 nodes · 8 unique messages · 0 servers · 0 internet
starling demo:crdt runs five "stations" sharing a situation board (a ReplicatedMap CRDT). It splits the network, edits the same key on both sides at once, heals — and every replica converges to one identical board, the conflict resolved the same way everywhere:
2. Network splits. Both sides edit the SAME key (road.bridge) at once:
island A sees road.bridge = "CLOSED — flooding (relief-1)"
island B sees road.bridge = "ONE LANE (clinic)" (diverged, as expected)
3. Partition heals — CRDT merge reconciles everything:
→ convergence: ✓ all 5 replicas identical
const { ReplicatedMap } = require('starling-mesh');
const board = new ReplicatedMap({ node, doc: 'situation-board' });
board.on('change', ({ key, value }) => console.log(`${key} → ${value}`));
board.set('road.bridge', 'CLOSED — flooding'); // merges everywhere, no coordinatorAlso ships GSet, GCounter, PNCounter, LWWRegister, LWWMap, and ORSet as standalone mergeable types.
flowchart LR
subgraph Node["StarlingNode"]
ID["Identity<br/>Ed25519 · X25519"]
ST["Store<br/>content-addressed"]
GO["Gossip<br/>anti-entropy + rumor"]
end
GO <--> TR{{"Transport"}}
TR -->|production| UDP["UDP multicast<br/>(real LAN)"]
TR -->|tests / demo| MEM["In-memory network<br/>latency · loss · partitions"]
ID --> ST
ST <--> GO
The node logic is transport-agnostic: the identical gossip engine runs over real UDP multicast in the field and over a simulated in-memory network (configurable latency, loss, partitions) in tests and the demo — which is how convergence is proven without a building full of devices.
Two mechanisms spread every message:
- Rumor-mongering (push) — a new message is broadcast to neighbors with a hop count (TTL); they re-broadcast one hop further. Content-address dedup breaks loops; TTL bounds the blast radius.
- Anti-entropy (push-pull) — periodically, and whenever a peer appears, a node sends a digest of the ids it holds; the peer pushes what's missing and pulls what it lacks.
sequenceDiagram
participant A
participant B
A->>B: DIGEST { ids A holds }
B->>A: MSGS { messages A is missing }
B->>A: WANT { ids B is missing }
A->>B: MSGS { the requested messages }
Note over A,B: both stores identical → eventual consistency
Anti-entropy is what makes Starling delay-tolerant: even if no two nodes are ever online at once, a message ferried by an intermediary still reaches everyone. It also repairs whatever rumor push loses to drops, churn, and healed partitions.
Every message is signed (Ed25519) and content-addressed (its id is the SHA-256 of its signed fields), giving integrity, dedup, and authorship for free. Confidentiality is optional and layered on top:
| Property | How | Status |
|---|---|---|
| Integrity & authenticity | Ed25519 signature; author must be the fingerprint of pubkey |
✅ always |
| Tamper / forgery rejection | Recompute the content id; verify the signature | ✅ always |
| Channel encryption | Shared passphrase → HKDF-SHA256 → ChaCha20-Poly1305 (AES-GCM fallback) | 🔒 optional |
| Direct messages | Per-message ephemeral X25519 → ECDH → AEAD (forward secrecy) | 🔒 optional |
| Anti-spam | Hashcash proof-of-work bound to the content id | ⛏️ optional |
Encrypted bodies stay signed and content-addressed, so relays carry ciphertext they cannot read. Starling is honest about its limits — no anonymity, no Sybil resistance, no forward secrecy for passphrase channels (yet). Read the full threat model before relying on it.
| Centralized chat | SMS / cellular | Starling | |
|---|---|---|---|
| Works with no internet | ❌ | ❌ | ✅ |
| Survives infrastructure loss | ❌ | ❌ | ✅ |
| No account / phone number | ❌ | ❌ | ✅ |
| Delay-tolerant (store & forward) | ❌ | ❌ | ✅ |
| End-to-end encrypted | sometimes | ❌ | ✅ optional |
| Zero servers to run | ❌ | ❌ | ✅ |
const { StarlingNode, UdpTransport, createIdentity } = require('starling-mesh');
const identity = createIdentity();
const node = new StarlingNode({ identity, transport: new UdpTransport({ peerId: identity.peerId, name: 'kit' }) });
node.on('message', (m) => console.log(`${m.author.slice(0, 8)}: ${m.body}`));
await node.start();
node.publish('rescue', 'anyone near the east shelter?');Swap UdpTransport for MemoryTransport to run a whole network in one process — see sim/simulate.js.
Project layout
src/
identity.js Ed25519 + X25519 keys, signatures, fingerprints
message.js content-addressed, signed messages (+ optional PoW)
store.js dedup + capacity-bounded store (+ Bloom summary)
wire.js packet framing + MTU-aware batching
gossip.js anti-entropy + rumor propagation engine
crdt.js GSet/GCounter/PNCounter/LWW*/ORSet + ReplicatedMap
seal.js E2E channel encryption (HKDF + ChaCha20-Poly1305)
dm.js X25519 sealed-box direct messages (forward secrecy)
bloom.js Bloom-filter set summaries (double hashing)
pow.js hashcash proof-of-work (anti-spam)
node.js StarlingNode — composes it all
control.js local HTTP + SSE API for the dashboard
transport/ udp.js (real multicast) · memory.js (simulation)
bin/starling.js CLI: chat · serve · simulate · keygen
sim/ simulate.js (mesh) · crdt-demo.js (replicated state)
web/ zero-build dashboard (HTML/CSS/JS)
test/ 51 tests (node:test) · docs/ PROTOCOL.md · THREAT_MODEL.md
npm test # 51 tests on node:test, zero installCovering identity & signatures, message integrity/forgery, the store, wire framing, the CRDT merge laws, channel encryption and sealed-box direct messages (round-trip, wrong-recipient, tamper, forward secrecy), Bloom-filter accuracy, proof-of-work, and end-to-end protocol behavior: a message crossing a multi-hop line, a partition healing via anti-entropy, forged messages rejected, CRDT state converging across a partition, ciphertext relays can't read, and under-powered messages dropped.
Shipped: CRDTs over the mesh · pre-shared-key channel encryption · X25519 forward-secret direct messages · Bloom-filter summaries · proof-of-work anti-spam.
Next: IBLT digests wired into live anti-entropy · group key agreement with ratcheting · more transports (Bluetooth LE, LoRa, WebRTC for browser meshes) · persistent store · per-author rate limiting · binary (CBOR) codec.
Issues and PRs welcome — see CONTRIBUTING.md and the Code of Conduct. Great first contributions: a new transport, a Bloom-filter-backed digest path, or protocol fuzz tests. Security reports: SECURITY.md.
MIT — use it, fork it, ship it where it might keep people connected.
npm run art.