A production-ready real-time chat backend built with NestJS, PostgreSQL, and Socket.io. Supports direct messaging, group chats, file uploads, message delivery tracking, and JWT authentication with refresh token rotation.
| Layer |
Technology |
| Framework |
NestJS 11 (TypeScript) |
| Database |
PostgreSQL + Prisma ORM 6 |
| Real-time |
Socket.io 4 (WebSockets) |
| Authentication |
JWT + Passport.js (access + refresh tokens) |
| Validation |
class-validator + class-transformer |
| File Upload |
Multer |
| Rate Limiting |
@nestjs/throttler |
| Password Hashing |
bcryptjs (salt 12) |
- Authentication — Register, login, logout, and silent token refresh with rotation
- Direct Chats — 1-to-1 private conversations
- Group Chats — Multi-user rooms with admin controls (add/remove members, promote roles)
- Real-time Messaging — WebSocket gateway for instant message delivery
- Message Lifecycle — Send, edit, and soft-delete messages
- Message Status — SENT → DELIVERED → READ tracking per user
- Typing Indicators — Broadcast typing start/stop events to room members
- Online Status — Track and broadcast user presence in real time
- File Uploads — Images and documents up to 10MB, served as static files
- Cursor Pagination — Efficient infinite-scroll pagination for chats and messages
- Role-Based Access — USER, MODERATOR, ADMIN roles with route guards
- Rate Limiting — Per-endpoint throttling to prevent abuse
src/
├── auth/ # JWT authentication (register, login, refresh, logout)
│ ├── dto/ # Request validation DTOs
│ └── strategies/ # Passport JWT & refresh token strategies
├── users/ # User profiles, search, online status
├── chats/ # Direct & group chat management
├── messages/ # Message CRUD + file upload
├── gateway/ # Socket.io WebSocket gateway
├── common/
│ ├── guards/ # JwtAuthGuard, WsAuthGuard, RolesGuard
│ ├── decorators/ # @CurrentUser(), @Roles()
│ ├── filters/ # WebSocket exception filter
│ └── interceptors/ # Response transform interceptor
├── prisma/ # Prisma client service + module
└── uploads/ # Served static files directory
- Node.js 18+
- PostgreSQL 14+
- npm
git clone <repo-url>
cd chat-api
npm install
Edit .env:
PORT=3000
CORS_ORIGIN=http://localhost:5173
DATABASE_URL=postgresql://user:password@localhost:5432/chat_api?schema=public
JWT_ACCESS_SECRET=your-super-secret-access-key-change-in-production
JWT_ACCESS_EXPIRES_IN=15m
JWT_REFRESH_SECRET=your-super-secret-refresh-key-change-in-production
JWT_REFRESH_EXPIRES_IN=30d
# Run migrations
npx prisma migrate dev
# Open Prisma Studio (optional)
npx prisma studio
# Development (watch mode)
npm run start:dev
# Production
npm run build
npm run start:prod
The API will be available at http://localhost:3000/api/v1.
Base URL: /api/v1
| Method |
Endpoint |
Auth |
Description |
| POST |
/auth/register |
— |
Create a new account |
| POST |
/auth/login |
— |
Login and receive tokens |
| POST |
/auth/refresh |
Refresh token |
Rotate refresh token |
| POST |
/auth/logout |
Bearer |
Invalidate refresh token |
| Method |
Endpoint |
Auth |
Description |
| GET |
/users/me |
Bearer |
Get current user profile |
| PATCH |
/users/me |
Bearer |
Update username or avatar |
| GET |
/users/search?q= |
Bearer |
Search users by query |
| GET |
/users/:username |
Bearer |
Get user by username |
| Method |
Endpoint |
Auth |
Description |
| GET |
/chats |
Bearer |
List user's chats (paginated) |
| POST |
/chats/direct |
Bearer |
Start a direct chat |
| POST |
/chats/groups |
Bearer |
Create a group chat |
| GET |
/chats/:id |
Bearer |
Get chat details |
| PATCH |
/chats/:id |
Bearer (admin) |
Update group info |
| DELETE |
/chats/:id |
Bearer (admin) |
Delete group |
| POST |
/chats/:id/members |
Bearer (admin) |
Add member to group |
| DELETE |
/chats/:id/members/:userId |
Bearer (admin) |
Remove member |
| PATCH |
/chats/:id/members/:userId/role |
Bearer (admin) |
Promote/demote member |
| DELETE |
/chats/:id/leave |
Bearer |
Leave group |
| Method |
Endpoint |
Auth |
Description |
| GET |
/messages/:chatId |
Bearer |
Get messages (cursor paginated) |
| POST |
/messages |
Bearer |
Send a text message |
| POST |
/messages/upload |
Bearer |
Upload a file and send |
| PATCH |
/messages/:id |
Bearer |
Edit a message |
| DELETE |
/messages/:id |
Bearer |
Soft-delete a message |
| POST |
/messages/:chatId/read |
Bearer |
Mark messages as read |
Namespace: /chat
Connection: Pass JWT as auth.token in the Socket.io handshake.
| Event |
Payload |
Description |
message:send |
{ chatId, content, type, fileUrl?, fileName?, fileSize? } |
Send a message |
message:edit |
{ messageId, content } |
Edit a message |
message:delete |
{ messageId } |
Delete a message |
messages:read |
{ chatId, messageIds } |
Mark messages as read |
typing:start |
{ chatId } |
Notify room of typing |
typing:stop |
{ chatId } |
Stop typing notification |
| Event |
Payload |
Description |
message:new |
Message |
New message received |
message:edited |
Message |
Message was edited |
message:deleted |
{ messageId, chatId } |
Message was deleted |
messages:read |
{ chatId, userId, messageIds } |
Read receipt update |
typing:start |
{ chatId, userId, username } |
User started typing |
typing:stop |
{ chatId, userId } |
User stopped typing |
user:online |
{ userId } |
User came online |
user:offline |
{ userId, lastSeen } |
User went offline |
chat:member_joined |
{ chatId, member } |
Member added to group |
chat:member_left |
{ chatId, userId } |
Member left group |
User ──< ChatMember >── Chat ──< Message
│ │
└──< RefreshToken MessageStatus >──┘
| Model |
Key Fields |
| User |
id, email, username, passwordHash, role, avatar, isOnline, lastSeen |
| Chat |
id, type (DIRECT|GROUP), name, description, avatar |
| ChatMember |
chatId, userId, role (MEMBER|ADMIN), joinedAt |
| Message |
id, chatId, senderId, content, type (TEXT|IMAGE|FILE), isEdited, isDeleted |
| MessageStatus |
messageId, userId, status (SENT|DELIVERED|READ) |
| RefreshToken |
token, userId, expiresAt |
Register / Login
│
▼
Access Token (15m JWT) + Refresh Token (30d UUID, stored in DB)
│
▼
401 Unauthorized ──► POST /auth/refresh ──► New token pair issued
│
Old refresh token deleted (rotation prevents reuse)
- Access tokens are stateless JWTs validated by signature only
- Refresh tokens are UUIDs stored in the database and deleted on use
- Logout immediately invalidates the refresh token
| Endpoint |
Limit |
| Global |
100 requests / 60s |
POST /auth/register |
5 requests / 60s |
POST /auth/login |
10 requests / 60s |
Endpoint: POST /messages/upload (multipart/form-data)
- Max size: 10 MB
- Allowed types:
.jpg, .jpeg, .png, .gif, .webp, .pdf, .doc, .docx, .zip
- Served at:
GET /uploads/<filename>
| Field |
Rules |
| Email |
Valid email format, unique |
| Username |
3–20 chars, alphanumeric + underscore, unique |
| Password |
8–64 chars, requires uppercase, lowercase, and digit |
| Message content |
Max 4000 characters |
| Group name |
Max 100 characters |
# Unit tests
npm run test
# Watch mode
npm run test:watch
# Coverage report
npm run test:cov
# End-to-end tests
npm run test:e2e
| Variable |
Required |
Description |
PORT |
No |
HTTP server port (default: 3000) |
CORS_ORIGIN |
Yes |
Allowed frontend origin |
DATABASE_URL |
Yes |
PostgreSQL connection string |
JWT_ACCESS_SECRET |
Yes |
Secret for signing access tokens |
JWT_ACCESS_EXPIRES_IN |
No |
Access token TTL (default: 15m) |
JWT_REFRESH_SECRET |
Yes |
Secret for signing refresh tokens |
JWT_REFRESH_EXPIRES_IN |
No |
Refresh token TTL (default: 30d) |
MIT