Relay
Real-time 1:1 messaging for distributed tech teams, with delivery under 200ms.
- Status
- Live
- Role
- Full Stack Developer
- Team
- Solo
- Timeline
- 1 week

The problem
Distributed engineering teams need messaging that is fast and that handles authentication carefully.
Who it is for
Distributed engineering teams.
What I built
A real-time 1:1 messaging app with delivery under 200ms over Socket.io, dual-token JWT auth with refresh rotation and replay detection, presence, typing indicators, and cursor-paginated history. Turborepo monorepo with a Next.js frontend, an Express backend, and MongoDB.
Built with
- Frontend
- Next.js 16
- React 19
- Tailwind CSS 4
- Backend
- Node.js
- Express 5
- Mongoose 9
- Passport.js
- Real-time
- Socket.io
- Data
- MongoDB
- Auth
- JWT dual-token
- Google OAuth 2.0
- Nodemailer
- Infra
- Turborepo
- TypeScript
In detail
- Message delivery
- <200ms
- Build timeline
- 1 week
- Monorepo workspaces
- 3
- MongoDB collections
- 4
Key features
Dual-token JWT auth
3-day access tokens plus rotating httpOnly refresh cookies (7 days). Email + password signup with nodemailer-based email verification, and Google OAuth 2.0 with CSRF protection via a state cookie.
Token family revocation
Refresh tokens are hashed with SHA-256 before storage, so the raw value never persists. Replay detection invalidates the entire login chain, not just the reused token.
Real-time messaging
Instant delivery via Socket.io with a JWT-authenticated handshake. Delivery status runs sent, delivered, read with a double-tick UI, plus message edit and soft delete.
Paginated history
Cursor-based message history loading 30 messages per page, deduplicated across the socket and HTTP layers.
Presence and typing
Online, Away, and Offline status driven by the socket connect/disconnect lifecycle. Typing indicators debounce at 3 seconds and are broadcast only, never written to the database.
Conversation state
Per-participant unread counts, mute, and archive tracked in
participantMeta[] on each conversation.
Retro-futuristic UI
Dark mode only: amber accent (#F5A623), CRT scanline overlays, an
animated starfield, and ambient glow blobs.
No UI library
Every component is custom-built with inline styles, no external component library anywhere in the client.
Architecture
Relay is a Turborepo monorepo with three workspaces:
@relay/client: Next.js 16 (App Router), React 19, Tailwind CSS 4@relay/server: Express 5, Socket.io, Mongoose 9, Passport.js@relay/shared: common TypeScript types andSOCKET_EVENTSconstants
Real-time event flow
client connect → JWT auth middleware → isOnline = true, socketId saved
message:send → save to MongoDB → emit message:new to conversation room
typing:start/stop → broadcast to room (not persisted)
disconnect → isOnline = false, lastSeen = now, broadcast presence:update
message:read → update readBy[], broadcast message:status to sender
Auth token flow
Sign in
Email + password or Google OAuth issues a 3-day access token and a rotating httpOnly refresh cookie (7 days).
Client-side session
The access token lives in
localStorageorsessionStorage. On app mount,AuthProviderrestores it to axios headers and calls/sessionto validate.Rotation and revocation
Each refresh rotates the token; only the SHA-256 hash is stored. A replayed refresh token revokes the whole token family.
Decision
OAuth token via URL fragment
Google OAuth redirects to /homepage#accessToken=.... The fragment
approach avoids CORS issues with cross-origin cookie delivery to a SPA.
Note
Socket state without persistence
Presence and typing are pure socket lifecycle state: broadcast to the conversation room with zero database writes.
Data model
Four MongoDB collections: users, tokens, conversations, messages.
tokensstores only the SHA-256 hash of refresh tokens: the raw value never touches the DBconversations.participantMeta[]tracks unread count, muted, and archived state per usermessagespost-save hook updatesconversation.lastMessage,lastMessageAt, and increments unread counts for other participants
Outcome
Relay combines dual-token auth, socket-driven state, and a MongoDB data layer in a Turborepo monorepo. Every feature, from auth to typing indicators, runs against the real backend, with no mock data left.
Challenges
- JWT authentication across HTTP and WebSocket layers
- Token family revocation on replay detection
- Google OAuth token delivery to SPA without CORS issues
- Real-time message deduplication across socket and HTTP
What I learned
- Dual-token auth patterns with refresh token rotation
- WebSocket + REST hybrid architecture
- Turborepo monorepo coordination across client, server, and shared packages
- Socket-driven presence and typing state without DB writes

Have a role in mind?
Send me a message on X with the role and a link to the posting. My resume has the rest.