perwitwi# Serverless P2P Mesh Chat
A proof-of-concept Serverless P2P application that utilizes WebRTC for transport and self-organizing "routers" within the browser to facilitate local network discovery without a dedicated backend.
This project demonstrates a zero-infrastructure deployment model. It replaces what would conventionally require a hosted signaling server, a discovery service, and a presence registry with an application-level mesh that emerges from two stateless, public services.
- STUN as Namespace Discovery: Google's STUN server (
stun.l.google.com) is used not just for NAT traversal, but to extract the client's Public IP viasrflxICE candidates. This IP becomes the shared namespace key (myapp-{ip}-...) for "local" discovery. - Protocol-Based Leader Election: The PeerJS "ID Taken" error—normally a failure condition—is repurposed as a protocol primitive for router election. If
myapp-{ip}-1is taken, the client joins as a peer; if free, it becomes the router.
- Transport: WebRTC via PeerJS.
- Signaling: Public PeerJS server (
0.peerjs.com). - Deployment: Static PWA (HTML/JS/CSS only), zero backend logic.
To group peers on the same network, the app needs the Public IP.
- Primary (WebRTC STUN): Queries
stun.l.google.com:19302. The returnedsrflxcandidate contains the device's real public IP. This works on most networks (unless UDP 19302 is blocked). - Fallback (HTTP): If STUN fails (e.g., corporate proxies), the app falls back to
api.ipify.org. - Cellular: On cellular connections, discovery is disabled due to NAT volatility. The app defaults to "Persistent ID" mode only.
The app distinguishes between finding a peer and trusting a peer.
| ID Type | Format | Visibility | Purpose |
|---|---|---|---|
| Router ID | myapp-{ip}-1 |
Public | Deterministic anchor for the network mesh. |
| Discovery ID | myapp-{ip}-{discoveryUUID} |
Public | Broadcast to the router. Opaque (no name included). |
| Persistent ID | myapp-{persistentUUID} |
Private | Long-term identity. Only exchanged after connection acceptance. |
To prevent a saved contact from appearing as a stranger in the discovery list:
- discoveryUUID is generated once on first launch and stored locally.
- The Discovery ID (
myapp-{ip}-{discoveryUUID}) is anonymous. - When a peer checks in with the Router, they send their
friendlyNameanddiscoveryUUIDas data payload. - Merging Logic: The client parses the incoming registry. It extracts the
UUIDsuffix from the Discovery ID and checks it againstlocalStoragecontacts.- Match Found: The peer is marked as
onNetwork: truein the Saved Contacts list. - No Match: The peer appears in the New Peers list.
- Match Found: The peer is marked as
The "Router" is simply a browser tab that won the race to claim the deterministic ID myapp-{ip}-1.
- Attempt to register
myapp-{ip}-1. - Success: You are the Router. Initialize empty registry.
- Fail (ID Taken): Connect to
myapp-{ip}-1as a standard peer.
- Maintain Registry: Stores
{ discoveryID, friendlyname, lastSeen, discoveryUUID }. - Check-in: On new peer connection, add to registry and push full registry to all connected peers.
- Heartbeat: Pings all peers every 60s. Removes non-responders and pushes updated registry.
- Local Cache: All peers maintain a full copy of the registry.
- TTL: Cache entries have a 90s TTL.
- Re-Election: If the router goes offline (ping fails):
- Peers wait a random jitter delay (0–3s).
- Peers attempt to claim
myapp-{ip}-1. - Winner: Becomes new router, imports its local cache as the new source of truth, and requests re-checkins.
- Losers: Re-connect to the new router.
Peers automatically discover each other via the Router registry push.
A connection request is made to a Discovery ID.
- Peer A sends:
{ type: 'request', friendlyname: 'John' } - Peer B prompts user (Accept/Reject).
- On Accept:
- Peer B sends:
{ type: 'accepted', persistentID: 'myapp-uuid-B', discoveryUUID: '...' } - Peer A responds:
{ type: 'confirm', persistentID: 'myapp-uuid-A', discoveryUUID: '...' }
- Peer B sends:
- Result: Both peers store each other's Persistent ID and Discovery UUID. All future communication occurs via Persistent ID.
If a known peer is not on the local network (Router registry):
- They appear under Saved Contacts.
- User can click Ping.
- App attempts a direct WebRTC connection to their stored
Persistent ID.
The peer list is strictly divided to handle the visibility logic:
🌐 ON THIS NETWORK
Contains both known contacts (merged via UUID match) and unknown strangers.
- 💬 John
[● on network][Open Chat] - 👤 Unknown
[Connect]
💾 SAVED CONTACTS
Contacts stored in localStorage but not currently in the local registry.
- 💬 Mike
[○ offline][Ping] - 💬 Sarah
[○ offline][Ping]
Peer → Router
{ type: 'checkin', discoveryID: '...', friendlyname: '...' }
{ type: 'ping' } // Keepalive=======================================================================================================================
NEW IDEADS:
This document outlines the architectural evolution from simple P2P ID sharing to a robust, cryptographic Zero-Trust model with self-healing connectivity.
We are fundamentally changing the security model. We no longer rely on PeerJS IDs for persistence or trust. Instead, we adopt a system where Transport is Ephemeral and Identity is Cryptographic.
- The Principle: A PeerJS ID is just a temporary "IP address." A user's ECDSA Key Pair is their permanent "Passport."
Trust is established strictly through cryptographic proof, not ID ownership.
- Challenge: When Alice connects to Bob (regardless of which PeerJS ID she uses), she sends her Public Key and a Digital Signature of that key.
- Verification: Bob verifies the signature. This mathematically proves the sender possesses the Private Key associated with that identity.
- Result: If valid, Bob updates his local contact list: "The identity [AlicePubKey] is currently located at Transport ID [myapp-random-123]."
The concept of a "Persistent PeerJS ID" is deprecated.
| Type | Format | Visibility | Purpose |
|---|---|---|---|
| Transport ID | myapp-{randomUUID} |
Public | An ephemeral, session-specific routing address. Semi-persistent; only changes if fails to re-register on peerjs. No trust value. |
| Discovery ID | myapp-{namespace}-{randomUUID} |
Public | A temporary address used to announce presence to a local discovery router (IP/Geo), and inform it of its current transport id. |
| Identity | <base64-PublicKey> |
Private | The user's permanent identity. Exchanged via Transport IDs after a trusted handshake. |
This feature is an opt-in backup mechanism for "Special Contacts." It allows trusted peers to find each other even if their Transport IDs are lost, squatted, or changed, without requiring a central server.
- The Concept: Two peers generate a Shared Secret during their initial connection. This secret is used to calculate a predictable, rotating Rendezvous Namespace based on the current time.
- Mesh Router Integration: Crucially, this calculated string functions exactly like a Discovery Namespace.
- Peers do not just "connect" to the ID.
- They utilize the app's existing Router Election Logic within this private namespace (e.g., claiming
{RendezvousHash}-1). - This ensures that even if both peers come online simultaneously (use jitter 1-3s to avoid crashes), one becomes the Router and the other acts as the Peer, guaranteeing a successful meeting.
- Shared Secret: A 256-bit secret exchanged once during setup.
- Universal Time Slots: Fixed 10-minute UTC intervals (e.g.,
xx:00,xx:10,xx:20). - Namespace Generation:
const timeSlot = 'UTC-YYYY-MM-DD-HH-' + Math.floor(minutes / 10); const rendezvousHash = HMAC_SHA256(SharedSecret, timeSlot); const namespace = `rendezvous-${rendezvousHash}`; // Router ID becomes: myapp-{namespace}-1
Clients maintain a strict state for each Special Contact.
- Rule: The client communicates directly via the contact's last known Transport ID. The rendezvous system is idle.
- Trigger: Direct connection to the Transport ID fails.
- Action: The client calculates the current Rendezvous Namespace and attempts to join it (either as Router or Peer).
- Goal: Find the contact in this private mesh, exchange new Transport IDs, and return to State 1.
- Trigger: The contact's Transport ID returns a valid PeerJS connection but fails the Cryptographic Identity check (Imposter/Squatter).
- Action: The Transport ID is blacklisted. The client immediately forces a switch to the Rendezvous Namespace to re-establish a secure link.
- Trigger: The client's own Transport ID changes.
- Action: It immediately announces the new Transport ID to all online contacts. For offline contacts, it joins the current Rendezvous Namespace to "leave a note" with its new address.
- Rule: If a Special Contact is unseen for >30 days, the app prompts the user to pause the rendezvous contract to save background resources.
This feature replaces the static myapp root prefix with a dynamic, server-verified token.
- The Concept: The application derives its root namespace variable from a cryptographic signature provided by the origin web server headers (
X-Mesh-Beacon). - The Goal: Mitigation, not perfection. It raises the barrier to entry for spammers, unauthorized bot clones, and generic PeerJS scanners.
- The Beacon: The web server signs the current timestamp with a private key and attaches it to response headers.
- The Check: The server only provides this header if the request
Originmatches the official domain (CORS). - The Namespace: The client uses this signature as the root prefix (e.g.,
sig8a2b-{ip}-1instead ofmyapp-{ip}-1).
- Not a DRM Solution: A determined attacker can manually extract the token and share it, or build a proxy to leak it. However, because the token rotates periodically (e.g., every 10 minutes), an attacker must maintain active infrastructure to bypass it, preventing low-effort scripts and "saved-to-disk" local copies from flooding the public mesh.