A place to exchange what you know for something you want to learn.
A peer-to-peer skill-exchange platform with matching, swap requests, persistent messaging, client-side encrypted chat, and LiveKit video calls. I owned the backend and real-time systems.
Follow one message from a client, through the Go hub, to its recipient.
A
CLIENT AOffers GoEncrypt here
Go hubCIPHERTEXTWaiting
B
CLIENT BOffers designDecrypt here
One connection. Something new to learn.ENCRYPTED BETWEEN CLIENTS
WEBSOCKET + CLIENT-SIDE ENCRYPTION
Explore a message's journey, from one client to another.
Local illustration only. This demo does not send a message or perform encryption. The project uses TweetNaCl for client-side encryption.
Start with the problem.
Matching two people is only the start. A useful exchange needs a conversation that survives refreshes, behaves sensibly across browser tabs, and can move from a message to a call without losing context.
The team built a Next.js frontend and a layered Go API. My focus was the backend and real-time layer: authentication and persistence, the WebSocket hub, encrypted-payload handling, and video-call integration.
My part in the system.
I owned the backend and real-time systems, not the entire product. The visual frontend and client-side cryptography belong to the wider team-built system. The backend supports those clients by persisting messages, relaying ciphertext, managing connections, and issuing LiveKit room tokens.
FIG. 02 / FAN-OUT
A user is more than one socket.
One persisted message can need delivery to the recipient and every active tab owned by the sender.
USER A / 2 CONNECTIONS
Tab 01sent
Tab 02synced
Go hubpersist, then fan out
USER B / 1 CONNECTION
Tab 01received
The hub tracks connections by user, not one socket per account. Ciphertext remains opaque to the relay.
Inside the system.
01 / A user is not a single connection
The Go WebSocket hub maps each user to multiple active connections. A single goroutine handles hub state, fans out messages to a user's connected tabs, and removes slow connections when their outgoing buffers fill.
The tradeoff. A process-local hub keeps connection ownership straightforward. It also means a second API instance cannot reach the first instance's sockets without adding cross-instance messaging.
The chat path persists a message before broadcasting it to both participants. Temporary message IDs let the sender reconcile optimistic updates with the server-confirmed record. Typing, read events, and call signaling share the WebSocket channel.
The tradeoff. Persistence gives refreshes a durable message history, while optimistic updates keep the interface responsive. Clients still have to distinguish pending, confirmed, and failed messages.
03 / Relay ciphertext; integrate media infrastructure
The client uses TweetNaCl with X25519-derived shared keys and XSalsa20-Poly1305 encryption. The backend accepts opaque ciphertext rather than decrypting message bodies. For calls, a backend service creates conversation-based LiveKit rooms and room-scoped join tokens; WebSockets carry call signaling.
The tradeoff. Keeping encryption at the client separates the relay from message plaintext. Key management remains a client concern, and LiveKit integration is distinct from building a media server or claiming end-to-end encrypted video.
The real-time hub is process-local, not a horizontally distributed messaging system. The encryption implementation is not a claim of Signal-style forward secrecy, and encrypted chat does not imply end-to-end encrypted calls or attachments. This case study describes implementation, not a security audit.
Natural next steps are cross-instance message delivery, connection-recovery testing, and a focused authorization and key-management review. Those would establish clearer operational and security guarantees before describing the platform as production-ready.
The details are in the code.
The repository includes setup instructions, implementation details, and the surrounding architecture. Follow the data path, explore a service, or run the project locally.