Skip to content

Repository files navigation

Realtime Chat

A full-stack 1:1 realtime chat reference implementation built with NestJS, Next.js, Socket.IO, PostgreSQL, Prisma, and Redis.

Feature Highlights

  • Email/username authentication with JWT access and refresh tokens.
  • User search and direct conversation creation.
  • Persistent message history with cursor pagination.
  • Socket.IO realtime messaging with optimistic UI reconciliation through clientMessageId.
  • Typing indicators, online/offline presence, delivered/read receipts, and own-message edit/delete.
  • Redis-backed Socket.IO adapter, presence, and distributed rate limiting.
  • Server-side authorization for REST and WebSocket conversation membership.
  • Responsive Next.js chat UI using TanStack Query and Tabler Icons.

Why This Project Is Interesting

This repository focuses on practical realtime engineering decisions: idempotent message sends, optimistic reconciliation, cursor pagination, distributed presence, Socket.IO horizontal scaling through Redis Adapter, read receipts, and server-owned authorization. It is a starter/reference implementation, not a turnkey production messaging platform.

Demo

Watch the Realtime Chat demo

Tech Stack

  • Backend: Node.js, TypeScript, NestJS, Socket.IO, Prisma ORM, JWT, DTO validation.
  • Frontend: Next.js, React, TypeScript, Socket.IO Client, TanStack Query, Tabler Icons.
  • Infrastructure: npm workspaces, Docker Compose, PostgreSQL, Redis.
  • Testing: Node test runner, Supertest, Vitest, Testing Library.

Architecture

The monorepo is split into:

  • apps/api: NestJS API and Socket.IO gateway.
  • apps/web: Next.js client.
  • packages/shared: shared event names and safe cross-app types.

REST is used for authentication, user search, conversation management, message history, and message actions. Socket.IO is used for low-latency chat events such as new messages, typing, presence, and receipts.

See docs/ARCHITECTURE.md for a concise architecture note.

Realtime Architecture

  • Clients authenticate Socket.IO connections with the access token in socket.auth.token.
  • Conversations map to rooms named conversation:<conversationId>.
  • Users also join user:<userId> rooms for targeted presence broadcasts.
  • The server verifies membership before joining rooms, sending messages, marking receipts, or broadcasting typing.
  • The client creates clientMessageId values for optimistic messages; the backend stores them with a uniqueness constraint per sender to make retries idempotent.
  • Message state is reconciled from sending/failed UI state to persisted sent, delivered, and read state.

Horizontal Scaling

Multiple API instances can share the same PostgreSQL database and Redis instance. Redis powers the Socket.IO adapter, presence state, and rate limiting. If a load balancer allows long-polling transports, configure sticky sessions so all requests for one Socket.IO session reach the same API instance. With WebSocket-only transport, that requirement is reduced.

Quick Start

Requirements: Node.js 24 or compatible, npm 11, Docker, and Docker Compose.

git clone https://github.com/Ricardo-NM/realtime-chat
cd realtime-chat
npm install
cp .env.example .env
docker compose up -d
npm run prisma:generate -w @realtime-chat/api
npm run prisma:migrate -w @realtime-chat/api -- --name init
npm run dev

On Windows PowerShell, use this instead of cp:

Copy-Item .env.example .env

Default local URLs:

  • Web: http://localhost:3000
  • API: http://localhost:4000
  • Health: http://localhost:4000/health
  • Readiness: http://localhost:4000/ready

Environment Variables

Copy .env.example to .env for local development. The example contains local-only placeholder JWT secrets so the app can run after cloning. Replace them before any shared, staged, or public deployment.

Variable Purpose
NODE_ENV Runtime mode. Production enables stricter env validation.
API_PORT NestJS API port.
WEB_PORT Reserved local web port value; the current web script runs Next on 3000.
INSTANCE_ID Optional API instance label for logs and scaling tests.
DATABASE_URL PostgreSQL connection string used by Prisma.
REDIS_URL Redis connection string.
FRONTEND_URL Comma-separated CORS allowlist for API and Socket.IO.
NEXT_PUBLIC_API_URL Browser-visible REST API URL.
NEXT_PUBLIC_SOCKET_URL Browser-visible Socket.IO URL.
JWT_ACCESS_SECRET Access token signing secret.
JWT_REFRESH_SECRET Refresh token signing secret. Must differ from access secret in production.
JWT_ACCESS_EXPIRES_IN Access token lifetime, for example 15m.
JWT_REFRESH_EXPIRES_IN Refresh token lifetime, for example 30d.

Database Migrations

Generate Prisma Client:

npm run prisma:generate -w @realtime-chat/api

Apply migrations in development:

npm run prisma:migrate -w @realtime-chat/api -- --name init

Useful Prisma checks:

npm run prisma:format -w @realtime-chat/api
npm run prisma:validate -w @realtime-chat/api

Development Commands

npm run dev
npm run dev:api
npm run dev:web
npm run format
npm run lint
npm run typecheck
npm run test
npm run build

Testing

Start PostgreSQL and Redis before running the full suite:

docker compose up -d
npm run test

The root test script runs API e2e/integration tests and web unit/component tests. API tests require DATABASE_URL, REDIS_URL, and JWT secrets from .env.

Project Structure

realtime-chat/
├── apps/
│   ├── api/          # NestJS REST API, Socket.IO gateway, Prisma schema
│   └── web/          # Next.js chat client
├── packages/
│   └── shared/       # Shared event constants and public types
├── docs/
│   ├── ARCHITECTURE.md
│   ├── SPEC.md
│   ├── images/
│   ├── media/
│   └── stages/
├── docker-compose.yml
├── package.json
└── README.md

API Reference

All protected endpoints use Authorization: Bearer <accessToken>.

Method Path Purpose
GET /, /health Basic API status.
GET /ready PostgreSQL/Redis readiness status.
POST /auth/register Register a user.
POST /auth/login Login with email/username and password.
POST /auth/refresh Rotate refresh token and issue new tokens.
POST /auth/logout Revoke the provided refresh token.
GET /auth/me Return the authenticated user.
GET /users/search Search users.
GET /users/:id Get a public user profile.
GET /conversations List conversations for the authenticated user.
POST /conversations Create or return a direct conversation.
GET /conversations/:id Get a conversation by id if the user is a member.
GET /conversations/:conversationId/messages List message history with cursor pagination.
PATCH /messages/:messageId Edit an own message.
DELETE /messages/:messageId Soft-delete an own message.
POST /presence/status Fetch presence status for users.

Socket.IO Events

Client-to-server events:

Event Payload
conversation:join { conversationId }
conversation:leave { conversationId }
message:send { conversationId, content, clientMessageId }
message:delivered { conversationId, messageId }
conversation:read { conversationId, upToMessageId }
typing:start { conversationId }
typing:stop { conversationId }

Server-to-client events:

Event Purpose
connection:ready Confirms authenticated socket connection.
message:new Broadcasts a persisted message.
message:delivered Broadcasts delivery receipt state.
message:read Broadcasts read receipt state.
message:edited Broadcasts edited message state.
message:deleted Broadcasts soft-deleted message state.
typing:start, typing:stop Broadcast typing state to other conversation members.
presence:online, presence:offline Broadcast relevant user presence transitions.

Canonical event names and TypeScript payloads live in packages/shared/src/index.ts.

Security Notes

  • Passwords are hashed with bcrypt before storage.
  • JWT identity is server-derived; clients cannot choose user identity by ID.
  • REST and Socket.IO paths validate conversation membership on the server.
  • Helmet, CORS allowlisting, payload limits, DTO validation, and public exception filtering are enabled.
  • HTTP and WebSocket rate limits use Redis with an in-process fallback for isolated Redis failures.
  • The current frontend stores access and refresh tokens in localStorage. This is acceptable for a learning/reference app but carries XSS risk. For real public deployments, migrate refresh tokens to HttpOnly, Secure, SameSite cookies and keep access tokens in memory.
  • Never commit .env files or real secrets.

Known Limitations

  • Auth tokens are currently stored in localStorage.
  • Groups are modeled but not complete as a user-facing feature.
  • No uploads, images, files, audio messages, object storage, reactions, push notifications, calls, or E2E encryption.
  • Redis high availability is not configured by this repository.
  • Sticky sessions may be required by the load balancer when long-polling transports are enabled.
  • lastMessage in the conversation list remains null.
  • No browser E2E suite is currently configured.
  • This is a starter/reference implementation, not a turnkey production messaging platform.

Roadmap

  • Complete group conversations.
  • Add file/image uploads backed by object storage.
  • Move refresh tokens to HttpOnly cookies for production-style auth.
  • Add browser E2E coverage for the core chat flow.
  • Add notifications and richer message interactions.
  • Document deployment examples without coupling the project to one provider.

Contributing

Contributions are welcome if they stay within the project scope and keep the reference implementation clear. See CONTRIBUTING.md.

License

MIT. See LICENSE.

About

Scalable realtime chat starter built with Next.js, NestJS, Socket.IO, PostgreSQL, Prisma and Redis — featuring optimistic messaging, typing, distributed presence, read receipts and horizontal scaling.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages