Skip to content

Bluebook722/starling

Starling — decentralized mesh messaging for when the network goes dark

CI Zero dependencies Node CRDTs E2E License

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


Why

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.

Highlights

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

Quickstart

npm install -g starling-mesh        # or run from a clone — there is nothing to build
starling 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:8730

Encrypted channels and direct messages are one flag / one call away:

starling chat --channel ops --secret "correct horse battery staple"   # 🔒 end-to-end encrypted channel
node.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.

See it converge

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

Conflict-free replicated state

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 coordinator

Also ships GSet, GCounter, PNCounter, LWWRegister, LWWMap, and ORSet as standalone mergeable types.

How it works

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
Loading

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:

  1. 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.
  2. 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
Loading

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.

Security model

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.

Why not just…

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

Use it as a library

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

Tests

npm test     # 51 tests on node:test, zero install

Covering 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.

Roadmap

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.

Contributing

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.

License

MIT — use it, fork it, ship it where it might keep people connected.

Built with zero dependencies on Node's standard library. The banner art regenerates with npm run art.

About

Decentralized, offline-first mesh messaging. Zero infrastructure, end-to-end signed, store-and-forward. Stay connected when the network goes dark.

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors