Architecture
Room lifecycle
On this page
Every Pug Network implementation MUST follow this six-step sequence. The steps are deliberately small, the order is fixed, and the contracts at each step are identical between the JavaScript and Go builds.
1. Challenge
The client requests a Proof-of-Work challenge. The server returns an opaque token plus a difficulty target. The client computes a SHA-256 partial-prefix PoW at 18-bit difficulty (≈262 000 hashes on average) and submits the nonce.
PoW makes automated room-creation spam computationally expensive without requiring identity. A human waits a fraction of a second; a bot trying to spin up thousands of rooms pays a serious CPU bill.
2. Create
The server validates the PoW, then:
- Clamps the requested TTL into the deployment's allowed range.
- Clamps the requested capacity into the deployment's allowed range.
- Generates a fresh opaque room ID and a 16-byte room secret.
- Stores the room (including its TTL timer) in the in-memory registry.
- Returns the room ID, room secret, expiry timestamp, and capacity to the client.
The client assembles the join URL of the form
/r/<roomID>#k=<AES key>&s=<roomSecret>.
Both the AES key and the room secret live in the URL fragment and are
never sent back to the server.
3. Join
For every joiner (including the creator), the server checks, in order:
- Room exists.
- Room has not expired.
- Room is not full.
- The supplied room secret matches in constant time — no early exit on mismatch, no length leak.
On success, the connection is added to the room, an ephemeral display name is assigned, and presence is broadcast to other members. The first successful joiner is recorded as the room creator; only that connection is later allowed to issue an explicit purge.
4. Message
The client encrypts plaintext with AES-256-GCM using the in-fragment key. The resulting ciphertext blob is sent over the WebSocket as opaque bytes; the server broadcasts it to current room members without inspection.
The server cannot decrypt, search, redact, log, or otherwise interpret the payload. From its perspective each message is a few hundred bytes of random-looking data that must be forwarded.
5. Leave
Connection close immediately removes the member from the registry and broadcasts a leave event. There is no grace period and no resurrection — a reconnecting client must re-join, re-supply the room secret, and pass all checks again from scratch.
6. Purge
A purge happens in three situations:
- Empty — the last participant left.
- TTL fired — the room reached its expiry timestamp.
- Explicit — the creator hit Purge.
In all three cases, all state — members, timers, lookup entries — is removed atomically. Active clients receive a close frame and the room ID becomes invalid. Partial cleanup is a bug.
A room MUST NOT outlive its TTL. Cleanup MUST remove timers, members, and lookup entries together. If you can find a state where one of these is true and the others are not, you have found a bug — please report it via Report a vulnerability.
End-to-end flow
Creator (browser) Server Joiner (browser)
│ │ │
│── GET /api/pow-challenge ▶│ │
│◀────── token + difficulty │ │
│ │ │
│── POST /api/pow-redeem ──▶│ verify PoW │
│◀────── sessionToken ──────│ │
│ │ │
│── GET /api/create-room ──▶│ generate roomID + secret │
│◀── roomID, secret, exp ───│ store in registry │
│ │ │
│ build URL with #k=…&s=… │ │
│ share URL out-of-band ─────────────────────────────▶│
│ │ │
│ │◀── room:join { roomId, │
│ │ roomSecret } ──────│
│ │ const-time check │
│ │── ok, username ─────────▶│
│ │ │
│── chat:message ciphertext▶│ broadcast ──────────────▶│
│◀───────── ciphertext ─────│ │
│ │ │
│ leave / purge / ttl ─────▶│ remove room atomically │
│ │── room:expired ─────────▶│
│ │ │