Super Secret Chat
Realtime chat over Socket.io with rooms, message edit/delete, and live presence. Dockerised from day one so dev and prod stayed identical.
Dockerised realtime messaging
- TypeScript
- React
- Node.js
- Socket.io
- Docker
Why I built this
I’d built a lot of features that used real-time without actually implementing the websocket side myself. Pusher, Firebase, Supabase Realtime. They all hide the lifecycle, the room model, the broadcast logic.
This was the smallest project I could come up with that exercises all of that without becoming a Slack clone.
What it does
- Rooms. Users join a named room and only see messages from that room.
- Edit and delete on messages. When someone edits, everyone in the room sees the change and a small “edited” marker.
- Presence notifications when someone joins, leaves, or edits.
docker-compose up --buildruns the same setup the production deploy uses.
Architecture
There’s one Express + Socket.io process acting as the gateway. State lives in memory. The room registry and the message log are JavaScript objects, not a database. When a client reconnects, the gateway re-emits the current room state so they catch up.
Technical decisions
Socket.io over a raw WebSocket. Reconnection, room semantics,
and event-based dispatch are all built in. A raw ws server would
have been a couple hundred lines of plumbing for the same thing.
In-memory state. The first version doesn’t need durability. When the server restarts, room history clears. Fine for a prototype.
Docker first. docker-compose up --build is the only way I
run it. There’s no separate “for prod” config.
Mutations are broadcast as patches. Edits and deletes send a patch instead of a replacement. Everyone sees the same view of history including the edited marker.
What I learned
Socket rooms aren’t really a primitive. They’re a convention built on top of namespaced emit calls. Knowing that early made the room registry a five-line map instead of something more complicated.
Reconnection is what reveals weak invariants. If a client drops mid-edit, every assumption you made about ordering shows up.
“Edits broadcast to everyone” is a product decision, not a technical one. Worth saying that out loud somewhere.
Next steps
- Persist rooms and messages to Postgres, or SQLite plus Litestream for the small case. So history survives a restart.
- Authentication via passkey or magic link. Right now anyone with the URL can join any room.
- Optional E2E encryption for room contents. Signal-style ratchet, with the gateway as a blind relay.
- Pull the realtime client into a small npm package. The reconnection and snapshot replay logic is reusable.