An ephemeral multi-room WebSocket chat server in Rust, on Axum 0.8. Visit a URL, get a room. Get an animal name instead of an account. Rooms with nobody talking in them are deleted. There is no database — all state is in memory and all of it is meant to disappear.
Live at https://thehellisthis.com.
cargo run # → http://localhost:3000/main
cargo build --release # LTO'd, stripped: 5.4 MB
PORT=3000 ./target/release/infinite-chat- Real-time multi-room WebSocket chat
- Markdown rendering with HTML sanitisation (comrak → ammonia)
- Anonymous identity: a UUID and an animal name, in
HttpOnlycookies - Rooms created by visiting them; reply threads, typing indicators, read receipts
Deliberate product decisions, not resource management.
- Ephemeral rooms vanish 5 minutes after the last person leaves them empty — a spark, not a hearth
- Idle users are disconnected after 10 minutes without speaking, so the user count means "people actually here"
maincan't be deleted, so its history fades instead — patiently, after 30 minutes of room-wide silence
- Global memory ceiling (150 MB) with pruning that refuses rather than grows
- Per-user rate limiting (30 messages/min, plus a short burst limit)
- Per-IP connection limits and temporary bans for repeated rejected attempts
- Graceful shutdown on SIGINT that announces departures before exiting
HttpOnly,SameSite=Strict,Secureidentity cookies; the client learns who it is from theWelcomeframe, never fromdocument.cookie- HTML sanitisation applied to rendered output, not to input
- Content-Security-Policy with
script-src 'self'— no inline script anywhere - HSTS,
X-Frame-Options: DENY,nosniff, Referrer-Policy, Permissions-Policy - WebSocket
Originchecking (upgrades are not covered by the same-origin policy, so the server has to enforce it) - 8 KB message cap, 512 KB frame cap, bounded and sanitised quoted replies
src/
main.rs Entry point, router, housekeeping loops — wiring only
config.rs Every tunable, with the reasoning attached
animals.rs The animal-name roster (sorted and deduped, enforced by test)
protocol.rs The WebSocket wire format
error.rs ChatError → HTTP status
identity.rs Identity cookies and their extractor
limits.rs Rate limits, memory tracking, connection pool, IP bans
room.rs RoomState, UserData, history trimming, name assignment
state.rs AppState, shutdown, periodic cleanup
validation.rs Input validation, Markdown → sanitised HTML
routes.rs HTTP handlers
session.rs WebSocket lifecycle: admission, four tasks, teardown
cleanup.rs Room housekeeping pass
emoji.rs Reaction emoji roster (the closed set a reaction may be)
security.rs Security headers, CSP, WebSocket origin policy
tests.rs 612 tests
index.html The page, compiled into the binary
client.js The client script, served from /app.js so the CSP can
forbid inline script entirely
cloudflare/ Worker + Container deployment
{ "type": "Message", "text": "hello" }
{ "type": "Typing", "is_typing": true }
{ "type": "ReadReceipt", "message_id": "<uuid>" }Welcome is always the first frame.
{ "type": "Welcome", "user_id": "<uuid>", "animal_name": "otter" }
{ "type": "Message", "message": { "message_id": "<uuid>", "user_id": "...", "animal_name": "otter", "text": "<sanitised html>", "timestamp": "<ms>" } }
{ "type": "System", "event": { "UserJoined": { "user_id": "...", "animal_name": "otter" } } }
{ "type": "UserCount", "count": 5 }
{ "type": "Heartbeat" }
{ "type": "ReconnectToken", "token": "<uuid>" }| Route | Purpose |
|---|---|
GET / |
308 → /main |
GET /main |
main room |
GET /:room |
dynamic room |
GET /health |
liveness probe |
GET /metrics |
Prometheus text exposition — requires Authorization: Bearer $METRICS_TOKEN, 404s if unset |
GET /admin |
dashboard listing every open room as a link — requires HTTP Basic auth against $ADMIN_TOKEN, 401s if unset |
WS /ws/:room |
WebSocket upgrade |
Every tunable lives in src/config.rs with a comment explaining its value. The ones worth knowing:
| Setting | Value |
|---|---|
MAX_ROOMS |
50 |
MAX_USERS_PER_ROOM |
100 |
MAX_CONCURRENT_USERS |
400 |
MAX_MESSAGES_PER_ROOM |
500 |
MAX_TOTAL_ROOMS_MEMORY |
150 MB |
MAX_MESSAGE_LEN |
8 KB |
MAX_CONCURRENT_CONNECTIONS_PER_IP |
3 |
HEARTBEAT_INTERVAL |
5s |
EMPTY_ROOM_CLEANUP_DELAY |
5 min |
USER_IDLE_MESSAGE_TIMEOUT |
10 min |
MAIN_ROOM_FADE_IDLE |
30 min |
cargo test --all-features # 612 tests
cargo test --all-features roster # a subset
scripts/coverage.sh # line-coverage floor (95%)
cargo mutants # mutation testing (manual sweep, slow)Covers room lifecycle, rate limiting, memory accounting and pruning, connection accounting, IP bans, message sanitisation and XSS attempts, cookie handling, the frontend/backend constant contract, and full WebSocket integration against a real server with a real client.
These four commands are what CI runs. All must pass before committing.
cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all-features
cargo build --release
scripts/coverage.shPushing to main deploys, after CI passes
(.github/workflows/deploy.yml). To deploy
manually:
./deploy.shThe Rust server runs in a Cloudflare Container behind a Worker that proxies both
HTTP and WebSocket traffic. max_instances is 1: all state is in process
memory, so a second instance would be a second, separate set of rooms behind the
same URLs.
- CLAUDE.md — architecture, critical constraints, protocol, pitfalls
- ENGINEERING-STANDARDS.md — the rulebook: the complexity, lock-discipline, memory, protocol, and blast-radius laws, and why each exists
MIT