@shuhaib-enfin/chat-server
v1.1.0
Published
Drop-in NestJS chat module — Socket.IO gateway, MongoDB persistence, file uploads, presence, calls. Mount via ChatModule.forRoot({ apiKey }).
Readme
@shuhaib-enfin/chat-server
NestJS chat module — Socket.IO gateway, MongoDB persistence, file uploads, presence, and audio calls.
Install
npm install @shuhaib-enfin/chat-server @shuhaib-enfin/chat-sharedQuick Start (CLI)
For quick local testing:
npx chat-server start --apiKey=chat_xxx --port=3002Options:
--apiKey(required): Your API key--port(default: 3002): Server port--mongoUri(default: mongodb://localhost:27017/chat_sdk): MongoDB connection string--mode(default: managed):managedorexternal-db
Mount in Your NestJS App
Option 1: Fresh MongoDB (no existing Mongoose)
Use ChatMongooseModule to create the connection, then mount ChatModule:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { ChatMongooseModule, ChatModule } from '@shuhaib-enfin/chat-server';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
ChatMongooseModule.forRoot(process.env.MONGO_URI || 'mongodb://localhost:27017/myapp'),
ChatModule.forRoot({
apiKey: process.env.CHAT_API_KEY || 'chat_xxx',
}),
],
})
export class AppModule {}Option 2: Already Have Mongoose
If your app already calls MongooseModule.forRoot(uri), skip ChatMongooseModule:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { ChatModule } from '@shuhaib-enfin/chat-server';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
MongooseModule.forRoot('mongodb://localhost:27017/myapp'),
ChatModule.forRoot({
apiKey: process.env.CHAT_API_KEY || 'chat_xxx',
}),
],
})
export class AppModule {}ChatModule registers its schemas on whatever Mongoose connection exists.
ChatModuleOptions
| Option | Type | Required | Description |
|--------|------|----------|-------------|
| apiKey | string | Yes | Your platform API key |
| mongoUri | string | No | Customer-provided DB (external-db mode) |
| mode | 'managed' | 'external-db' | No | Default: managed |
| version | string | No | API version string |
| platformMongoUri | string | No | Platform DB for key validation |
| uploadDir | string | No | Where uploaded files are written. Relative paths resolve against process.cwd(). Overrides UPLOAD_DIR env. |
| fileUploadHandler | function | No | Custom upload handler. Receives the parsed file and returns the URL. Use for S3 / Azure / GCS. See Custom file upload handler. |
Modes
managed (default)
The chat server validates API keys against its own database. Use this when you control the keys and want the server to manage everything.
external-db
The customer provides their own MongoDB. The server stores chat data there but still validates keys against your platform. Use this for customers who want isolation.
Environment Variables
| Variable | Description |
|----------|-------------|
| MONGO_URI | Fallback MongoDB URI (used if not provided in options) |
| PORT | Server port (default: 3002) |
| UPLOAD_DIR | Where uploaded files are written. Relative paths resolve against process.cwd() (e.g. public/images → <cwd>/public/images). Absolute paths are used as-is. Default: uploads (i.e. <cwd>/uploads). |
Configuring the upload directory
Resolution order: options.uploadDir → process.env.UPLOAD_DIR → uploads (relative to cwd).
# Default: <cwd>/uploads
node dist/main.js
# Relative path: <cwd>/public/images (the file URL is still /uploads/<uuid>.<ext>)
UPLOAD_DIR=public/images node dist/main.js
# Absolute path
UPLOAD_DIR=/var/data/chat-uploads node dist/main.jsOr in NestJS options:
ChatModule.forRoot({
apiKey: process.env.CHAT_API_KEY!,
uploadDir: 'public/images', // resolves to <cwd>/public/images
}),The directory is created automatically on startup if it does not exist. Uploaded files are served at GET /uploads/<filename> (handled by ChatModule — no extra useStaticAssets needed in your main.ts).
Custom file upload handler
For production deployments you usually want files in object storage (S3, Azure Blob, Google Cloud Storage) instead of on the chat server's disk. Pass a fileUploadHandler to ChatModule.forRoot to take over the upload step.
The handler receives the file (already parsed by multer — file.path is on local disk and file.buffer is in memory) plus the validated tenantId and roomId, and returns the URL where the file is now reachable. The URL is stored in the Message exactly like the default behaviour — no frontend changes are needed.
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { ChatModule, FileUploadHandler } from '@shuhaib-enfin/chat-server';
const s3 = new S3Client({ region: 'us-east-1' });
const uploadToS3: FileUploadHandler = async ({ file, tenantId, roomId }) => {
const key = `${tenantId}/${roomId}/${Date.now()}-${file.originalname}`;
await s3.send(new PutObjectCommand({
Bucket: 'my-chat-uploads',
Key: key,
Body: file.buffer ?? require('fs').createReadStream(file.path),
ContentType: file.mimetype,
}));
return {
fileUrl: `https://my-chat-uploads.s3.amazonaws.com/${key}`,
fileName: file.originalname,
fileType: file.mimetype,
fileSize: file.size,
};
};
ChatModule.forRoot({
apiKey: process.env.CHAT_API_KEY!,
fileUploadHandler: uploadToS3,
});Notes:
- If
fileUploadHandleris not provided, the default multer-to-disk behaviour is unchanged. - The handler is called after API key and room validation, so it only ever sees authorised uploads.
- Return any URL the frontend can
fetch— absolute (S3, CDN) or relative (your own CDN in front of the bucket). - For tenant isolation, namespace the storage key with
tenantId(as shown above) so tenants cannot read each other's files. - The built-in
GET /uploads/<filename>route is still registered but will be unused when you return external URLs. It is harmless to leave in place.
REST Endpoints
All endpoints require x-api-key header unless noted.
| Method | Path | Description |
|--------|-----|-------------|
| POST | /api/validation/validate | Validate API key |
| POST | /api/users/register | Register a user |
| GET | /api/users | List all users (with presence) |
| GET | /api/rooms | List rooms for a user |
| POST | /api/rooms/direct | Open/get direct room |
| GET | /api/rooms/:roomId/messages | Get room messages |
| POST | /api/upload | Upload file (multipart/form-data) |
Socket Events
Namespace: /chat
Connect
Handshake auth required:
socket = io('http://localhost:3002/chat', {
auth: { apiKey: 'chat_xxx', userId: 'u1', userName: 'Alice' }
});Client → Server
| Event | Payload | Description |
|-------|--------|-------------|
| message | { roomId, content } | Send message |
| typing | { roomId, isTyping } | Typing indicator |
| presence | { status } | Set presence (online/away/offline) |
| join-room | { roomId } | Join a room |
| leave-room | { roomId } | Leave a room |
| call | { to, offer } | WebRTC offer |
| call-accept | { callId, answer } | WebRTC answer |
| call-reject | { callId } | Reject call |
| call-end | { callId } | End call |
Server → Client
| Event | Payload | Description |
|-------|--------|-------------|
| message | { roomId, messages[] } | New message(s) |
| typing | { roomId, userId, isTyping } | User typing |
| presence:changed | { userId, status } | Presence update |
| room:updated | { room } | Room updated |
| call | { callId, from, offer } | Incoming call |
| call-accepted | { callId, answer } | Call accepted |
| call-rejected | { callId } | Call rejected |
| call-ended | { callId } | Call ended |
| error | { code, message } | Error event |
| users:list | { users[] } | User list on join |
File Uploads
POST to /api/upload with multipart/form-data:
- Field:
file - Header:
x-api-key - Body:
{ roomId: string }
Response:
{
"success": true,
"fileUrl": "/uploads/uuid-filename.jpg",
"fileName": "photo.jpg",
"fileType": "image/jpeg",
"fileSize": 12345
}Max file size: 50MB. Files are served at GET /uploads/<filename> by the SDK's built-in controller — you do not need to register useStaticAssets in your main.ts.
Production: the built-in disk storage is for development and small deployments. For production pass a
fileUploadHandlertoChatModule.forRootthat streams the file to S3 / Azure Blob / GCS and returns the public URL. See Custom file upload handler.
Troubleshooting
EADDRINUSE
Another process is on your port. Kill it or use a different port:
# Find process
netstat -ano | findstr :3002
# Kill on Windows
taskkill /PID <pid> /FMongoDB connection failed
Ensure MongoDB is running and the URI is correct. If using Docker:
docker run -d -p 27017:27017 mongoInvalid apiKey
Keys must exist in your platform database. In managed mode, the server looks up the key on startup. In external-db mode, provide valid keys in your customer's DB.
For frontend SDK, see @shuhaib-enfin/chat.
