Skip to main content
Projects

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
Relay screenshot

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 and SOCKET_EVENTS constants

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

  1. Sign in

    Email + password or Google OAuth issues a 3-day access token and a rotating httpOnly refresh cookie (7 days).

  2. Client-side session

    The access token lives in localStorage or sessionStorage. On app mount, AuthProvider restores it to axios headers and calls /session to validate.

  3. 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.

  • tokens stores only the SHA-256 hash of refresh tokens: the raw value never touches the DB
  • conversations.participantMeta[] tracks unread count, muted, and archived state per user
  • messages post-save hook updates conversation.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.