Architecture
Architecture
On this page
Pug Network is three cooperating layers. Each has a single responsibility, each can be reasoned about in isolation, and the encryption boundary lives above all three of them — nothing the server runs ever holds plaintext.
The three layers
Browser A Browser B
┌──────────┐ ┌──────────┐
│ AES-GCM │ encrypt │ AES-GCM │
│ encrypt │ │ decrypt │
└────┬─────┘ └────▲─────┘
│ │
─── encryption boundary ─────────────────────── encryption boundary ───
│ ciphertext │
v │
┌──────────────────────────────────────────────┐
│ Realtime Gateway (WebSocket relay only) │
└────────────────────┬─────────────────────────┘
│
v
┌──────────────────────────────────────────────┐
│ Room Registry (in-memory, TTL'd) │
└────────────────────▲─────────────────────────┘
│
│
┌──────────────────────────────────────────────┐
│ HTTP / API (serve client, create rooms) │
└──────────────────────────────────────────────┘
Per-layer responsibilities
| Layer | Responsibility | JS implementation | Go implementation |
|---|---|---|---|
| HTTP / API | Serve client assets, issue PoW challenges, create rooms | Express | net/http + embed |
| Realtime gateway | Bidirectional ciphertext relay between members | Socket.IO | Hand-rolled RFC 6455 subset |
| Room registry | Membership, capacity, TTL, purge | In-memory Map |
In-memory map + sync.Mutex |
Domain model
Room
- Room ID — opaque random token; the only identifier the server uses.
- Room secret — 16 bytes of entropy, required for join, never leaves browsers.
- Created at / expires at — bounded TTL, validated against per-deployment limits.
- Capacity — bounded; enforced at join time.
- Members — set of live connections.
- Creator connection — the first joiner; only this connection may purge the room.
Member (connection)
- Connection ID — ephemeral, assigned at WebSocket accept, discarded on close.
- Display name — ephemeral, server-generated, never persisted.
Message event
- Sender — ephemeral connection ID.
- Room ID — for routing only.
- Payload — opaque AES-256-GCM ciphertext blob (server cannot inspect).
- Timestamp — server-assigned, used for ordering and display only.
Why this shape
The split is deliberate. Each layer holds the smallest amount of state it can possibly hold to do its job:
- The HTTP layer is stateless beyond a short-lived PoW challenge cache. It does not know what a "user" is.
- The realtime gateway is stateless beyond per-connection membership. It does not know what a "message history" is.
- The room registry is stateful, but only in memory and only in time-bounded form. It does not know what a "database" is.
This is what allows the trust model to make absolute claims. There is no layer that could persist plaintext if it tried — none of them ever see plaintext, and none of them have a disk-backed store to write to. See Trust model for the consequences.