Back to selected work
REAL-TIME WEB / TEAM PROJECT

SkillSwap.

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.

MY FOCUS

Backend & real-time systems

CONTEXT

Team project / Skill Issue

CORE TECHNOLOGIES

Go, Gin, PostgreSQL, WebSocket, LiveKit

Explore source
SKILLSWAP / REAL TIMELOCAL WALKTHROUGH

Where does the plaintext stop?

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.

Read internal/chat/hub.go

02 / Persist before broadcasting

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.

Read internal/chat/ws_handler.go

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.

Read internal/app/service/video_service.go

Where it stands. What comes next.

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.

View SkillSwap on GitHub or explore the service map.

NEXT PROJECT

DFMS