Across any network. No server holding the truth.
Most of what is called peer to peer keeps a server behind it — to introduce the devices to each other, to hold what they agree on, and to pass messages along when they cannot reach each other directly. Take the server away and the application stops.
Here every device keeps its own journal of what happened, and devices find each other by a cryptographic identity rather than by an address. The network helps them find a path to each other. When a direct path isn't available, a relay forwards packets it cannot read.
No server holds the truth, because the truth is already on the devices.
Get the codeWhat it does
What Puppeteer gives the application
The substrate we build on: every device keeps its own journal of what happened, and each decision has one writer.
A transport that can change under the rules
The rules know nothing about devices or networks, so what carries the messages can be replaced without changing a line of them.
See what changed, and what did notThe same logic in an app and in a browser
The same rules and the same subject run in a native Android app and, compiled for the browser, in a page that installs like one.
See the phone and the browserNothing stops when one device goes down
Every device holds its own part of the history, so the others carry on: players keep their game and sellers keep selling.
See a player with the organizer downOne authority where it matters
Each decision has one writer, so no two devices sell the same item or award the same prize; a sale made while it was away is checked when it is back.
See a sale go to the organizerDevices that were away catch up, in order
A device that joins late, or comes back after a drop, receives what it missed in the order it happened; nothing is reconciled by hand.
See a phone come back
What iroh gives the connection
The transport, reached through an adapter we wrote. The rules and the journals above do not depend on it.
Devices that find each other by identity
A device is its public key, not its address: nothing is opened on a router, and the link is encrypted end to end, even through a relay that forwards packets it cannot read.
See the transportA room with no internet still works
Phones on the hall's network reach each other directly; nothing outside the room has to be reachable.
See the hall with no internetConnections that survive a change of network
Wi-Fi to mobile data changes the path, not the device: the session carries on, or picks up where it left off.
See a change of network
Where it fits
Three examples. What they have in common is a shape, not an industry: a group of devices that has to agree on something while the network cannot be relied on.
Live events in a venue
RunDraws, raffles, auctions, quizzes, votes. The room is the network, and the evening does not depend on anything outside it.
See it run: The Village HallPop-up sales and markets
Several sellers and one inventory: each sale has one writer, so no two devices sell the same item — and a seller keeps selling while that writer is out of reach.
Field work with patchy coverage
Teams keep working through dead zones and catch up, in order, when a path comes back.
These are examples and not a list. The capabilities above are general; what changes from one case to the next is how much of the network you can count on.
Three layers
The rules
The game, the sale, the prize, in plain .NET: it knows nothing about Puppeteer, devices or networks.
No devices · no networks · no Puppeteer
The subjects
Puppeteer gives each subject an identity and a journal. Its Stage Manager keeps one writer per decision and replicas that replay its history; a tell carries a fact from one subject to another.
Identity · journal · replay
The transport
An adapter carries the messages. Here it is iroh over QUIC; elsewhere the same model runs over sockets on a local network or a broker in a data center.
Carries messages
- Hereiroh over QUIC
- ElsewhereSockets on a local network
- ElsewhereA broker in a data center
Ball 42 · told once per link: three sealed copies, not a broadcast
The transport can change without changing the domain or the subject model. In September it did: the transport moved from HTTPS between phones to iroh, and the game did not change a line. Its domain tests never mention a network.
iroh is the adapter we chose for phones across networks. The adapter is ours; the model does not depend on it.
Trust here is not a promise anybody makes. A device is its own key, a relay forwards packets it cannot read, and a journal can be replayed by anyone holding it. What has to be trusted is cryptography and a record — not a party, and not us.
What Puppeteer isSince September: iroh underneath · the rules and the subject did not change a line
What keeps working when a piece goes down.
Every phone holds its own part of the evening, so one piece going down does not stop the others.
Organizer down · the card stays on the screen
Organizer down · sales are kept on the phone
Connected · each ball over its own link
No internet · balls go over the hall's network
An evening across four homes.
The round · 23 Sep 2026
- Four homes
- Four different networks
- Organizer in an emulator on a PC
- No port forwarding
- No tunnel
- Wi-Fi → mobile data, mid-round
- Back as the same device
- No private network
On 23 September a live bingo round ran over iroh, with players in their own homes, each behind a different router, and the organizer in an emulator on a PC. Nobody opened a port, installed a tunnel or arranged a private network.
One player switched between home Wi-Fi and mobile data mid-round: the link dropped and came back as the same device, with the same cards.
Nobody else in the round noticed.
When two devices cannot find a direct path, a relay introduces them. In that round it was a public one, run by somebody else: it could not read what it carried and kept none of it. It does not have to be somebody else's — a relay can be your own.
Measured on Android emulators. In the round itself, phones that dropped came back as the same device, as the timeline above shows.
Different network. Same night.
A phone on a home Wi-Fi joins an organizer it has no network route to: the organizer runs inside an emulator, behind two layers of NAT. Through the transport adapter we wrote for Puppeteer, iroh finds the organizer by its identity; the player signs up over that link, gets its card and marks the next ball half a second later.
Galaxy A54 on home Wi-Fi and an Android emulator · 25 Sep 2026 · one pause of about two seconds cut
What was measured, and on what.
Eleven results, each with the device it was taken on and the day it was taken. Four were taken on emulators and two come from a prototype rather than the published web client, and each of those says so.
| What | Result | Measured on |
|---|---|---|
| Phones on different home networks | Four phones on four networks over QUIC through iroh, with no port opened on any router | Android phones · 23 Sep 2026 |
| Longest single connection in a live round | 22 minutes, through all 69 draws | Android phone on a home network · 23 Sep 2026 |
| Every draw of the live round, over mobile data onlyEmulator | 69 of 69 reached the control device, which could reach the organizer only over mobile data | Android emulator · 23 Sep 2026 |
| Wi-Fi to mobile dataEmulator | Next event in the same second; six seconds after two switches in a row | Android emulators · 23 Sep 2026 |
| A room with no internetEmulator | Events delivered in zero to two seconds over direct paths | Android emulators · 23 Sep 2026 |
| Back after an outageEmulator | Reconnected by itself 10 to 52 seconds after the network returned | Android emulator · 23 Sep 2026 |
| The browser as a peerPrototype | Events pushed over a channel the browser opens outward, 58 to 237 ms round trip | Galaxy A54, Chrome 153 · 23 Sep 2026 |
| The browser back from the backgroundPrototype | Reconnected 4 and 12 seconds after returning to view — the handshake itself takes half a second — and received the 13 and the 8 draws it had missed | Galaxy A54, Chrome 153 · 23 Sep 2026 |
| The web client | Installs as an Android app over HTTPS, and from there reads the organizer over the local network | Galaxy A54, Chrome 153 · 24 Sep 2026 |
| The web client with its host unreachable | Opened from its cached copy and kept talking | Desktop Chrome 153 · 24 Sep 2026 |
| First visit of the web client | 11.6 MB gzipped — 9.6 MB of it the runtime and the engine, and 0.15 MB the page | Build output · 24 Sep 2026 |
Every row above has a raw output behind it, with the hour it was taken and the command that produced it.
What to plan for
- A browser cannot accept incoming connections; it has to open the channel itself.
- Phones freeze apps in the background within seconds, so plan for reconnecting and catching up, not for a connection that stays.
- Across networks a relay is needed when no direct path exists — a public one or your own; inside one room it is not.
- Safari on iPhone will not let a page served over HTTPS reach devices on the local network.
- The behaviour holds on the devices we tried. Scale is the next question.
Questions
How do I build a peer-to-peer app without a server?
Give every device its own journal and let the devices exchange facts directly. One subject writes each decision and its replicas share the history, so no server has to hold the truth.
Can peer-to-peer apps connect across different networks without port forwarding?
Yes. Devices identify each other by a public key rather than an address and connect over QUIC through iroh, with NAT traversal first and a relay that cannot read the traffic as the fallback.
What happens when a phone switches from Wi-Fi to mobile data?
The device keeps its identity: on emulators the connection carried on over the new path; on phones in a live round, the link dropped and came back as the same device, and caught up.
Can a peer-to-peer app work on a local network with no internet?
Yes. Devices in the same room reach each other over direct local paths, and nothing outside the room takes part.
How do devices catch up after being offline?
They receive what they missed, in the order it happened. A device that joins late receives everything that already happened.
Is peer-to-peer traffic encrypted when it goes through a relay?
Yes. The connection is authenticated and encrypted end to end between the devices, and the relay only forwards packets it cannot read.
Can a web browser be a peer?
Partly. The same logic runs in the browser and the page installs like an app, but a browser cannot accept incoming connections — it has to open a channel outward.
What is iroh?
An open-source library for direct, encrypted connections between devices over QUIC. A device is dialed by its public key; iroh prefers a direct path, found by NAT hole punching, and keeps the traffic on a relay only when there is none. Puppeteer reaches it through a transport adapter we wrote.
Does iroh work on Android?
Yes. The bingo runs it inside a .NET app on Android phones: on 23 September four phones on four home networks joined one round this way, with no port opened on any router.
How do devices stay consistent without a central server?
Each decision has one writer, which checks every write before recording it in its journal. Devices that work alongside it keep a replica that replays that history in the same order; the rest are told each fact as it happens. Reads come from each device's own copy, so writes and reads take separate paths, as in CQRS.
Is this event sourcing?
Yes, in the sense that matters here: each device's state is its journal replayed, and a mistake is corrected by a new entry, never by editing an old one. What the journal records is the operation itself rather than an event describing its outcome, so replaying it runs it again.
How is this different from CRDT sync?
CRDTs let every device write and merge the results afterwards. Here each decision has one writer, so there is nothing to merge: replicas receive the same history in the same order, and two devices cannot both sell the same card.