Skip to content

Commit 46c5c50

Browse files
committed
docs:add README with demo gif
1 parent 38a67eb commit 46c5c50

3 files changed

Lines changed: 153 additions & 70 deletions

File tree

ARCHITECTURE.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# ZeroRelay Architecture
2+
3+
## Overview
4+
5+
ZeroRelay is a P2P opensource mesh app for sharing files and text directly between browsers. No central database. Each peer owns its own copy of data in IndexedDB (Dexie). The signaling server (Cloudflare Durable Object) only helps peers find each other and exchange WebRTC SDP. Data flows directly browser-to-browser over WebRTC data channels.
6+
7+
```mermaid
8+
graph TD
9+
DO[Cloudflare DO<br/>per room] -->|signaling| PeerA
10+
DO -->|signaling| PeerB
11+
DO -->|signaling| PeerC
12+
PeerA <-->|WebRTC| PeerB
13+
PeerB <-->|WebRTC| PeerC
14+
PeerA <-->|WebRTC| PeerC
15+
```
16+
17+
## Stack
18+
19+
| Layer | Technology |
20+
|---|---|
21+
| Frontend | Next.js 15 (App Router) |
22+
| State | Zustand |
23+
| Persistence | Dexie v4 (IndexedDB) |
24+
| P2P | WebRTC (full mesh) |
25+
| Signaling | Cloudflare Workers + Durable Objects |
26+
| Styling | Tailwind CSS v4 |
27+
28+
29+
## Key Stores
30+
31+
| Store | What it holds |
32+
|---|---|
33+
| `useRoomStore` | peerId, name, peers[], connected, error |
34+
| `useUIStore` | room list, activeRoomId, dialog state (persisted to localStorage) |
35+
| `useMessageStore` | SharedItem[] in memory, download progress |
36+
| `useAvatarStore` | peerId -> base64 dataUrl map |
37+
38+
## Signaling Handshake
39+
40+
```mermaid
41+
sequenceDiagram
42+
participant A as Peer A
43+
participant S as Signaling Server (DO)
44+
participant B as Peer B
45+
46+
A->>S: WS connect / join { roomId, peerId, name }
47+
S->>A: joined { peerId, peers }
48+
S->>B: peer-joined { peer }
49+
A->>A: createOffer -> setLocalDescription -> waitForGathering()
50+
A->>S: offer { to: B, sdp (with all ICE candidates) }
51+
S->>B: offer { from: A, sdp }
52+
B->>B: setRemoteDescription -> createAnswer -> setLocalDescription -> waitForGathering()
53+
B->>S: answer { to: A, sdp }
54+
S->>A: answer { from: B, sdp }
55+
A->>A: setRemoteDescription -> data channel open
56+
```
57+
58+
The server only relays offer/answer messages. No trickle ICE — all candidates are bundled into the SDP before sending.
59+
60+
## Reconnection Flow
61+
62+
```mermaid
63+
sequenceDiagram
64+
participant C as Client
65+
participant S as Signaling Server
66+
67+
C->>C: WS close
68+
C->>C: store.setConnected(false)
69+
C->>C: disconnectAll() (clear stale RTCPeerConnections)
70+
Note over C: 2s reconnect timer
71+
C->>S: WS reconnect
72+
S->>C: joined { peerId, peers }
73+
C->>C: store.setConnected(true) -> connectToPeer(each)
74+
C->>C: fresh RTCPeerConnections created
75+
```
76+
77+
## WebRTC Connection Lifecycle
78+
79+
```mermaid
80+
stateDiagram-v2
81+
Idle --> Connecting: joined received
82+
Connecting --> Connected: data channel open
83+
Connected --> Idle: WS disconnect / disconnectAll()
84+
Connected --> Connecting: new peer joined
85+
```
86+
87+
Only the peer with the lower `peerId` initiates the offer. This avoids glare without extra coordination.
88+
89+
## Sharing Data
90+
91+
```
92+
Sender: shareItem(item, targetPeerId?)
93+
-> broadcast(item) via RTCDataChannel
94+
-> saveMessage(item) to local Dexie
95+
-> feed updates immediately (optimistic)
96+
97+
Receiver: onmessage -> parse DataMessage
98+
-> dedupe by id -> saveMessage(item) to local Dexie
99+
-> feed updates
100+
```
101+
102+
Each peer stores only what it has received. Retention (`session`, `5min`, `1h`, `1d`, `forever`) controls Dexie cleanup.
103+
104+
## Storage (per peer)
105+
106+
| Storage | Content |
107+
|---|---|
108+
| IndexedDB (Dexie) | `messages` table (SharedItem), `avatars` table (peerId -> dataUrl) |
109+
| localStorage | User name, room list (Zustand persist), theme |
110+
111+
## Signaling Server
112+
113+
A Cloudflare Worker routes `/api/ws/:roomId` to a Durable Object (one per room). The DO:
114+
- Validates passwords (first joiner sets it)
115+
- Relays offer/answer between peers
116+
- Broadcasts presence (peer-joined, peer-left, peer-renamed)
117+
- Broadcasts room-deleted
118+
119+
The DO never stores messages, avatars, or any shared data. It is purely in-memory.
120+
121+
## Key Design Decisions
122+
123+
- **Bundled ICE**: wait for `iceGatheringState === "complete"` before sending SDP. No trickle. Eliminates "Unknown ufrag" race conditions.
124+
- **Lower-peerId-wins**: deterministic tie-break for simultaneous offers. Peer with lower id initiates, higher id waits.
125+
- **Read roomId at send time**: `useRoomStore.getState().roomId` instead of passing as a prop. No stale roomId in messages.
126+
- **Full disconnect on WS drop**: `onDisconnect` -> `setConnected(false)` -> `disconnectAll()` clears stale PCs so reconnect creates fresh ones.
127+
- **No server storage**: DO is in-memory only. Messages live in each peer's Dexie.

README.md

Lines changed: 26 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,27 @@
1-
##
21
<p align="center">
3-
<em>screenshot placeholder — replace with a GIF showing a room with peers, sharing text + files</em>
4-
<br><br>
5-
<!-- IMAGE TAG -->
2+
<img src="./demo.gif" alt="ZeroRelay Demo" width="800"/>
63
</p>
74

8-
---
95

10-
> **ZeroRelay** is a peer-to-peer mesh app for sharing text, code, notes, and files directly between browsers. **No servers store your data** — the signaling server only helps peers discover each other. Everything flows directly between browsers over WebRTC.
6+
<h1 align="center">ZeroRelay</h1>
7+
8+
> **ZeroRelay** is a peer-to-peer opensource mesh app for sharing text, code, notes, and files directly between browsers. **No servers store your data** - the signaling server only helps peers discover each other. Everything flows directly between browsers over WebRTC.
119
12-
<br>
10+
<br/>
1311

14-
## Features
12+
## Features
1513

1614
| | |
1715
|---|---|
18-
| **P2P Mesh** | Every peer connects directly to every other peer — no central database |
19-
| **Password Rooms** | Room-level passwords enforced by Cloudflare Durable Objects |
20-
| **Share Any Type** | Chat, code (monospace), notes (italic), or files of any size |
21-
| **Retention Model** | Per-item: session, 5min, 1h, 1d, or forever |
22-
| **Profile Avatars** | Upload an avatar — it's exchanged once when a new peer connects |
23-
| **LAN Detection** | Global room is restricted to LAN peers automatically |
24-
| **Dark + Light** | Full Tailwind theme support |
25-
| **Zero Backend DB** | IndexedDB on each peer for persistence — nothing stored on the server |
16+
| **P2P Mesh** | Every peer connects directly to every other peer - no central database |
17+
| **Password Rooms** | Room-level passwords enforced by Cloudflare Durable Objects |
18+
| **Share Any Type** | Chat, code (monospace), notes (italic), or files of any size |
19+
| **Retention Model** | Per-item: session, 5min, 1h, 1d, or forever |
20+
| **Zero Backend DB** | IndexedDB on each peer for persistence - nothing stored on the server |
2621

2722
---
2823

29-
## Quick Start
24+
## Quick Start
3025

3126
```bash
3227
# 1. Install dependencies
@@ -40,69 +35,30 @@ npm run dev:worker
4035

4136
# 4. Open http://localhost:3000 in two browser windows
4237
```
38+
4339
---
4440

45-
## Architecture in 30 Seconds
41+
## Architecture
4642

47-
```
48-
┌─────────────────┐
49-
│ Cloudflare DO │ ← only relays ICE/SDP
50-
│ (per room) │
51-
└────────┬────────┘
52-
│ signaling
53-
┌──────────────┼──────────────┐
54-
▼ ▼ ▼
55-
┌────────┐ ┌────────┐ ┌────────┐
56-
│ Peer A │◄──►│ Peer B │◄──►│ Peer C │ ← WebRTC mesh
57-
│(Dexie) │ │(Dexie) │ │(Dexie) │ (data channels)
58-
└────────┘ └────────┘ └────────┘
43+
```mermaid
44+
graph TD
45+
DO[Cloudflare DO<br/>per room] -->|signaling| PeerA
46+
DO -->|signaling| PeerB
47+
DO -->|signaling| PeerC
48+
PeerA <-->|WebRTC| PeerB
49+
PeerB <-->|WebRTC| PeerC
50+
PeerA <-->|WebRTC| PeerC
5951
```
6052

6153
Each browser stores its own copy of everything in IndexedDB. When you share something, it's broadcast to all connected peers over WebRTC data channels. The signaling server never sees your messages, files, or avatars.
6254

6355
---
6456

65-
## Tech Stack
66-
67-
| Layer | Choice |
68-
|---|---|
69-
| **Frontend** | Next.js 15 + React 19 |
70-
| **State** | Zustand (in-memory + localStorage persist) |
71-
| **Persistence** | Dexie v4 (IndexedDB) |
72-
| **P2P Transport** | WebRTC (mesh topology) |
73-
| **Signaling** | Cloudflare Workers + Durable Objects |
74-
| **Styling** | Tailwind CSS v4 |
75-
| **Tooling** | Biome, TypeScript, Husky |
76-
77-
---
78-
79-
## Project Structure
80-
81-
```
82-
zerorelay/
83-
├── src/
84-
│ ├── components/ # React components
85-
│ │ ├── room/ # SharePanel, MessageFeed, PresenceList, UserCard
86-
│ │ └── ui/ # Avatar, Button, Dialog, Input (reusable)
87-
│ ├── hooks/ # useRoom, useSignaling, useWebRTC
88-
│ ├── lib/ # ice.ts, signaling.ts, db.ts, id.ts, file-transfer.ts
89-
│ ├── stores/ # Zustand stores (room, message, avatar, ui)
90-
│ ├── types/ # SharedItem, PeerMessage, Retention, etc.
91-
│ └── app/ # Next.js App Router entry
92-
├── workers/
93-
│ └── signaling/ # Cloudflare Worker + Durable Object
94-
├── shared/ # Protocol types shared between client + server
95-
└── ARCHITECTURE.md # Full architecture deep-dive
96-
```
97-
98-
---
99-
100-
## Documentation
57+
## Documentation
10158

102-
- **[ARCHITECTURE.md](ARCHITECTURE.md)** deep dive into data flow, storage, room lifecycle, retention, and security
59+
**[ARCHITECTURE.md](ARCHITECTURE.md)** - deep dive into data flow, storage, room lifecycle, retention, and security
10360

10461
---
10562

106-
## License
107-
108-
MIT
63+
## License
64+
[MIT](LICENSE)

demo.gif

5.33 MB
Loading

0 commit comments

Comments
 (0)