Skip to content
Pug Network Docs Open the app

Architecture

Room lifecycle

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:

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:

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:

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.

Invariant

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 ─────────▶│
       │                          │                          │