podcast manager
1#+PROPERTY: COOKIE_DATA recursive
2#+STARTUP: overview
3
4* concepts [0/5]
5
6the skypod architecture is broken into pieces:
7
8** p2p with realms
9
10In order to sync device and playback state, we need to have devices communicate with each
11other, which requires a signaling server, for peer discovery at the very least.
12
13*** realm
14
15A realm is a collection of known identities, verified with signed JWTs, where those
16identities can communicate outside of the server's purview.
17
18A realm is not publicly routable; to gain access, one must have the realm id and an
19invitation to the realm from an already existing member; new realms are created on
20demand with a random realmid supplied by the client.
21
22**** realm server
23
24A realm at the signalling sever is only a collection of known identity public keys, and
25the currently connected sockets.
26
27It acts mostly as a smart router; socket connection, authentication, and directed and
28realm-wide broadcast.
29
30**** realm client
31
32The realm client manages a full-mesh p2p network with other connected peers in the realm,
33and exposes the combined set of incoming messages as a stream that the client can do
34whatever with.
35
36***** realm encryption
37
38In order to keep private data off of the server, the realm client takes on the additional
39task of maintaining a shared encryption key for the realm, which can be used to encrypt
40data going over broadcasts.
41
42****** TODO key exchange protocol
43
44*** identity
45
46Identity in the realm system is just an id and keypair.
47
48The private key stays local to the installed device, and is used to send signed tokens
49over the wire, either to the realm server to manage authentication, or over other channels
50to other members in the realm.
51
52The /public/ key is stored by all members of the realm, and the server, in order to
53perform signature validation (which is also authentication).
54
55**** browser private key storage
56
57There is no good way to store private keys in a browser, but there are less bad ways.
58
59- private keys are ~CryptoKey~ objects, with ~{ exportable: false }~
60- WebCrypto native ~CryptoKey~ are structured clonable, which means they can get saved to indexeddb
61
62At the end of the day this is a podcast app.
63
64***** TODO are there other ways to do this?
65
66Could we use webauthn, or some way to use a yubikey to sign something?
67
68*** connection flow
69
70#+begin_src mermaid :file docs/readme/connection-flow.png
71 sequenceDiagram
72 participant Server as Server
73 actor Client as Client
74 participant PeerA as Existing Peer A
75 participant PeerB as Existing Peer B
76
77 Client<<->>Server: WebSocket Connection
78
79 Note over Client, Server: Authentication Phase (3 second timeout)
80 alt Registration (new realm)
81 Client->>Server: JWT: preauth.register {pubkey}
82 Server->>Server: validate JWT signature
83 Server->>Server: ensureRegisteredRealm(realmid, identid, pubkey)
84 else Authentication (existing realm)
85 Client->>Server: JWT: preauth.authn {}
86 Server->>Server: get publickey for issuer
87 Server->>Server: validate JWT signature
88 else Invitation Exchange
89 Client->>Server: JWT: preauth.exchange {inviteJwt, pubkey}
90 Server->>Server: get publickey for issuer
91 Server->>Server: validate invitation JWT signature & nonce
92 Server->>Server: admitToRealm(realmid, identid, pubkey)
93 Server->>Server: identity admitted
94 end
95
96 Note over PeerB, Server: Authenticated! Exchange Peer Identities
97 par do
98 Server->>Client: preauth.authn response {peers[], identities{}}
99 and
100 Server--)PeerA: realm.rtc.peer-joined {identid, pubkey}
101 and
102 Server--)PeerB: realm.rtc.peer-joined {identid, pubkey}
103 end
104
105 Note over Client, PeerB: WebRTC Connections (initiated by Client)
106 par do per connected peer
107 Client->>+Server: realm.rtc.signal {signed_jwt, localid, remoteid}
108 Server->>PeerA: realm.rtc.signal {signed_jwt, localid, remoteid}
109 PeerA->>PeerA: verify JWT signature (pubkey from `peer-joined`)
110 PeerA->>Server: realm.rtc.signal {signed_answer_jwt, localid, remoteid}
111 Server->>-Client: realm.rtc.signal {signed_answer_jwt, localid, remoteid}
112 Client->>Client: verify JWT signature (pubkey from `authn` response)
113
114 Note over Client, PeerA: Direct P2P Connection Established
115 Client->>PeerA: Direct WebRTC Connection
116 loop every 30s
117 PeerA<<->>Client: realm.rtc.ping/pong
118 PeerA<<->>Client: application messages
119 end
120 end
121
122 Note over PeerB, Server: Message Flow (Operational)
123 Client->>PeerA: Application Data (p2p)
124 Client->>PeerB: Application Data (p2p)
125 Client->>Server: realm.broadcast {payload, recipients}
126 Server->>PeerB: realm.broadcast {payload}
127
128 Note over PeerB, Server: Connection Persistence & Error Handling
129 alt Socket Error/Close
130 Client->>Client: Connection destroyed
131 Server->>PeerA: realm.rtc.peer-left {identid}
132 Server->>PeerB: realm.rtc.peer-left {identid}
133 PeerA->>PeerA: Disconnect peer
134 PeerB->>PeerB: Disconnect peer
135 else Peer Leaves
136 PeerA->>Server: Connection closes
137 Server->>Client: realm.rtc.peer-left {peerA_identid}
138 Server->>PeerB: realm.rtc.peer-left {peerA_identid}
139 Client->>Client: Disconnect from PeerA
140 end
141#+end_src
142
143#+RESULTS:
144[[file:docs/readme/connection-flow.png]]
145
146*** message format
147
148A message consists of:
149
150- ~typ~ :: the message type, see below
151- ~msg~ :: a message key, used for discrimination on the payload
152- ~seq?~ :: an (optional) sequence number, which allows for request/reply semantics
153- ~dat?~ :: a payload, the schema for which depends on ~typ~ and ~msg~ (possibly omitted)
154
155See ~#common/protocol~ for the messages that have been defined.
156
157**** ~typ~
158
159- ~evt~ :: events - one-way notifications
160- ~req~ :: requests - request-response pattern with sequence matching
161- ~res~ :: responses - correlated responses to requests by ~seq~
162- ~err~ :: errors - structured errors with HTTP-style status codes
163
164**** examples
165
166#+begin_example
167{
168 "typ": "req",
169 "msg": "preauth.register",
170 "seq": 1234,
171 "dat": {
172 "pubkey": { ... }
173 }
174}
175#+end_example
176
177#+begin_example
178{
179 "typ": "req",
180 "msg": "preauth.authn",
181 "seq": 1234
182}
183#+end_example
184
185#+begin_example
186{
187 "typ": "res",
188 "msg": "preauth.authn",
189 "seq": 1234,
190 "dat": {
191 "identities": { ... },
192 "peers": { ... }
193 }
194}
195#+end_example
196
197*** message authentication
198
199A client authenticates to the system and to peers by exchanging signed JWTs as the first
200messages in the messaging protocol; for authenticating to the realm, and for exchanging
201webrtc signaling messages.
202
203**** TODO webrtc encryption?
204
205The websocket is encrypted by virtue of ~wss~ and the server's cert, but I'm not sure if
206the traffic over webrtc is encrypted; if it's not, we should figure that out.
207
208** feed proxy server
209
210Due to ~CORS~, we'll need to help clients fetch the contents of feeds by running a caching
211proxy server for various HTTP requests.
212
213- help bypass ~CORS~ restrictions, so clients can access the content of the response
214- cache feeds, especially with regards to running transformations
215- perform transformations on responses:
216 - text feeds: reader mode, detect reading time
217 - podcast feeds: extract episode metadata, audio analysis for silence skips, etc
218 - all feeds: extract title tags, etc.
219
220*** TODO open question: is the client able to not use the proxy?
221
222I'm not sure yet if we want the PWA to be able to pull feeds directly when the server
223isn't present. It would be much easier to keep it around, but
224
225** feed management
226
227With a solid p2p WebRTC connection, we can use something like ~dexie~ or ~rxdb~ to get a
228synced document database that we use to manage feeds.
229
230* flow
231
232- user goes to https://skypod.accidental.cc
233 - pwa runs, prompts to do full install for storage and offline
234 - pwa is installed, sets up caches
235
236- first run
237 - identity is generated (id + keypair per device)
238 - do you want to sync to an existing install?
239 - if yes, go to invitee flow
240 - otherwise, new realm is generated and registered
241 - pubkey and id get stored in the realm, to make future sync easier
242
243- subsequent runs
244 - identity already exists, so we just go about our day
245
246- invitee flow
247 - already generated identity
248 - qr code pops
249 - scanned by inviter, see inviter flow
250 - done button after
251 - camera pops, scan inviter's QR codes
252 - sends invitation+registration token to server
253 - added to the realm
254 - go to subsequent runs
255
256* WebRTC Full-Mesh Implementation Plan
257
258** Overview
259
260Implement a full-mesh WebRTC system where every client in a realm establishes direct
261peer-to-peer connections with all other clients. The existing WebSocket infrastructure
262serves as the signaling channel, leveraging the newly refactored broadcast protocol.
263
264** Architecture Components
265
266*** Protocol Extensions (src/common/protocol/messages-rtc.js)
267
268Create new WebRTC message schemas:
269- rtc.offer - SDP offer with connectionId
270- rtc.answer - SDP answer with connectionId
271- rtc.ice-candidate - ICE candidate exchange
272- rtc.peer-state - Connection state updates
273- rtc.request-connection - Initiate connection with polite flag
274- rtc.peer-joined - Server message when peer joins (includes member list)
275- rtc.peer-left - Server message when peer leaves
276
277*** Server-Side Updates
278
279**** broadcastToRealm Function Enhancement
280
281Update the function signature to:
282- Take complete messages instead of payloads
283- Add skipSelf flag (default true)
284- For rtc.peer-joined, set skipSelf=false to include sender
285
286**** Handler Updates (handler-realm.js)
287
288- On client join: Broadcast rtc.peer-joined to ALL members (including self)
289- On client leave: Broadcast rtc.peer-left to remaining members
290- Existing broadcast mechanism handles WebRTC signaling perfectly
291
292*** WebRTC Utilities (src/common/webrtc.js)
293
294Core utilities for WebRTC:
295- RTC_CONFIG with STUN servers
296- DATA_CHANNEL_CONFIG for reliable messaging
297- PerfectNegotiation class for glare-free negotiation
298- ConnectionHealthMonitor for ping/pong health checks
299
300*** Client WebRTC Manager (src/client/webrtc-manager.js)
301
302Main orchestrator that:
303- Manages all peer connections
304- Handles incoming RTC messages
305- Routes signaling between peers
306- Emits events for UI updates
307- Provides public API for sending messages
308
309*** Client Peer Connection (src/client/peer-connection.js)
310
311Individual peer connection handler:
312- RTCPeerConnection lifecycle management
313- Perfect negotiation implementation
314- Data channel setup and messaging
315- Health monitoring with ping/pong
316- Automatic reconnection with exponential backoff
317- Connection state tracking
318
319*** UI Components
320
321**** PeerList Component
322- Shows all realm members
323- Connection status indicators
324- Real-time state updates
325
326**** MessageInterface Component
327- Send messages via WebRTC or server broadcast
328- Display incoming messages
329- Mode selection (P2P vs server relay)
330
331** Connection Flow
332
333*** Initial Join
3341. Client authenticates via WebSocket
3352. Server sends realm.status
3363. Server broadcasts rtc.peer-joined to ALL members
3374. Client sees own join message with member list
3385. Client initializes WebRTCManager
3396. Client connects to all existing members
340
341*** Peer-to-Peer Connection
3421. Initiator creates RTCPeerConnection (polite=true)
3432. Creates data channel, triggering negotiation
3443. Sends offer via realm.broadcast to target peer
3454. Target creates RTCPeerConnection (polite=false)
3465. Exchanges answer and ICE candidates
3476. Data channel opens, health monitoring starts
348
349*** Reconnection
3501. Health monitor detects issues or connection drops
3512. Exponential backoff timer starts
3523. New connection attempt with fresh connectionId
3534. ICE restart or full renegotiation
354
355** Key Design Decisions
356
357*** Perfect Negotiation Pattern
358Prevents glare when both peers try to connect simultaneously by using
359polite/impolite roles.
360
361*** Health Monitoring
362Proactive ping/pong messages detect connection issues before browser APIs,
363enabling faster recovery.
364
365*** Connection ID Tracking
366Each connection attempt has unique ID to ensure offer/answer pairs match
367during concurrent connections.
368
369*** Leveraging Existing Infrastructure
370WebRTC signaling is just another payload type in the existing broadcast
371system - no new server complexity.
372
373** Implementation Order
374
375*** Phase 1: Core Infrastructure
3761. Create protocol message schemas
3772. Update broadcastToRealm function
3783. Add peer join/leave broadcasts
3794. Create WebRTC utilities module
380
381*** Phase 2: Client Connection Management
3821. Implement WebRTCManager
3832. Create PeerConnection class
3843. Add perfect negotiation
3854. Implement health monitoring
386
387*** Phase 3: UI Integration
3881. Update main app to initialize WebRTC
3892. Create PeerList component
3903. Add MessageInterface with dual modes
3914. Style connection indicators
392
393*** Phase 4: Robustness
3941. Add reconnection logic
3952. Implement ICE restart
3963. Handle edge cases
3974. Add comprehensive error handling
398
399** Testing Strategy
400
401*** Unit Tests
402- Perfect negotiation scenarios
403- Health monitoring logic
404- Message routing
405
406*** Integration Tests
407- Full connection flow with mocks
408- Signaling message flow
409- State management
410
411*** E2E Tests
412- Real browser testing
413- Network condition simulation
414- Multi-peer scenarios
415
416*** Load Tests
417- Mesh scalability limits
418- Message throughput
419- Connection stability