Skip to content

Configuration

ChatModuleOptions

The ChatModuleOptions interface configures nestjs-chat's behavior when imported via ChatModule.forRoot().

typescript
interface ChatModuleOptions {
  database: {
    url: string;
  };
  redis: {
    url: string;
  };
  storage?: {
    maxFileSize?: number;
    allowedMimeTypes?: string[];
  };
  limits?: {
    maxChannelMembers?: number;
    maxPinnedMessages?: number;
    maxMessageLength?: number;
    maxPollOptions?: number;
  };
}

database (required)

FieldTypeDescription
urlstringPostgreSQL connection string. Format: postgresql://user:password@host:port/dbname?schema=public
typescript
database: {
  url: 'postgresql://postgres:postgres@localhost:5432/chat_service?schema=public',
}

Dedicated database

The database.url must point to a dedicated PostgreSQL database (or a separate schema in the same cluster) for chat data. The SDK owns its schema and runs its own migrations — pointing it at your main app's database will cause Prisma migration conflicts.

redis (required)

FieldTypeDescription
urlstringRedis connection string. Format: redis://[:password@]host:port[/db]

Redis is used internally by the SDK for two things:

  1. Scheduled messages: BullMQ job queue for delayed message delivery
  2. Socket.IO adapter: multi-instance event synchronization for horizontal scaling

No BullMQ setup required

The SDK manages BullMQ internally using bullmq directly (not @nestjs/bullmq). This avoids conflicts with your host application if it also uses BullMQ. You only need to provide the Redis URL — no BullModule.forRoot() or any other configuration.

typescript
redis: {
  url: 'redis://localhost:6379',
}

storage (optional)

Constraints on uploaded files. Only applies if an IChatStorageProvider is provided.

FieldTypeDefaultDescription
maxFileSizenumber10 * 1024 * 1024 (10 MB)Maximum file size in bytes
allowedMimeTypesstring[]['image/*', 'video/*', 'audio/*', 'application/pdf']Allowed MIME types (supports wildcards)
typescript
storage: {
  maxFileSize: 25 * 1024 * 1024, // 25 MB
  allowedMimeTypes: [
    'image/*',
    'video/*',
    'audio/*',
    'application/pdf',
    'application/vnd.openxmlformats-officedocument.*',
  ],
}

limits (optional)

Functional limits to prevent abuse.

FieldTypeDefaultDescription
maxChannelMembersnumber100Maximum number of members per group channel
maxPinnedMessagesnumber5Maximum number of pinned messages per channel
maxMessageLengthnumber5000Maximum message length in characters
maxPollOptionsnumber10Maximum number of options per poll
typescript
limits: {
  maxChannelMembers: 200,
  maxPinnedMessages: 10,
  maxMessageLength: 10000,
  maxPollOptions: 20,
}

logging (optional)

Winston logging configuration. The SDK writes to both console and file transports.

FieldTypeDefaultDescription
level'debug' | 'info' | 'warn' | 'error''info'Minimum log level
directorystring'logs'Directory for log files (chat-errors.log, chat-combined.log)
typescript
logging: {
  level: 'debug',
  directory: '/var/log/myapp',
}

Zero process.env

The SDK reads all its configuration from ChatModuleOptions. It never reads process.env directly — no DATABASE_URL, no REDIS_URL, no NODE_ENV, no LOG_DIR. Everything is explicit and passed by the host.

Exceeding limits

When a limit is reached, the service throws a ChatException with the appropriate code (CHAT_CHANNEL_MEMBER_LIMIT, CHAT_CHANNEL_PIN_LIMIT, etc.) and HTTP status 413.


ChatModuleProviders

The ChatModuleProviders interface defines the classes that the host injects into ChatModule.

typescript
interface ChatModuleProviders {
  authGuard: Type<IChatAuthGuard>;
  userExtractor: Type<IChatUserExtractor>;
  userResolver: Type<IChatUserResolver>;
  storageProvider?: Type<IChatStorageProvider>;
  eventHandler?: Type<IChatEventHandler>;
}

authGuard (required)

Implements IChatAuthGuard. Authentication validator for HTTP and WebSocket requests.

typescript
interface IChatAuthGuard {
  canActivate(context: ExecutionContext): boolean | Promise<boolean>;
}

The guard receives the standard NestJS ExecutionContext. It must return true if the request is authenticated, false or throw an exception otherwise.

typescript
@Injectable()
class JwtAuthGuard implements IChatAuthGuard {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.replace('Bearer ', '');
    if (!token) return false;
    try {
      const payload = await this.jwtService.verifyAsync(token);
      request.user = payload;
      return true;
    } catch {
      return false;
    }
  }
}

userExtractor (required)

Implements IChatUserExtractor. Extracts the current user from the request after the guard has validated authentication.

typescript
interface IChatUserExtractor {
  extractUser(request: any): ChatAuthUser | null;
}

interface ChatAuthUser {
  id: string;
  tenantId: string;
  email?: string;
  name?: string;
}

The tenantId field is critical

It ensures multi-tenant isolation. Every request must carry a tenantId. If your app is not multi-tenant, use a fixed value (e.g., 'default').

typescript
@Injectable()
class MyUserExtractor implements IChatUserExtractor {
  extractUser(request: any): ChatAuthUser | null {
    const user = request.user; // populated by the guard
    if (!user) return null;
    return {
      id: user.sub,
      tenantId: user.organizationId,
      email: user.email,
      name: `${user.firstName} ${user.lastName}`,
    };
  }
}

userResolver (required)

Implements IChatUserResolver. Resolves user profiles from their ID. Used to enrich responses (names, avatars) and for search.

typescript
interface IChatUserResolver {
  getUser(userId: string): Promise<ChatUser | null>;
  getUsers(userIds: string[]): Promise<ChatUser[]>;
  searchUsers(keyword: string, tenantId: string, limit?: number): Promise<ChatUser[]>;
  isOnline?(userId: string): Promise<boolean>;
}
MethodUsage
getUserSingle user profile (detail view)
getUsersBatch profiles (channel member list)
searchUsersSearch for adding members, mentions
isOnlinePresence indicator (optional)
typescript
@Injectable()
class MyUserResolver implements IChatUserResolver {
  constructor(private prisma: PrismaService) {}

  async getUser(userId: string): Promise<ChatUser | null> {
    const user = await this.prisma.user.findUnique({ where: { id: userId } });
    if (!user) return null;
    return {
      id: user.id,
      displayName: `${user.firstName} ${user.lastName}`,
      avatarUrl: user.avatarUrl,
    };
  }

  async getUsers(userIds: string[]): Promise<ChatUser[]> {
    const users = await this.prisma.user.findMany({
      where: { id: { in: userIds } },
    });
    return users.map(u => ({
      id: u.id,
      displayName: `${u.firstName} ${u.lastName}`,
      avatarUrl: u.avatarUrl,
    }));
  }

  async searchUsers(keyword: string, tenantId: string, limit = 20): Promise<ChatUser[]> {
    const users = await this.prisma.user.findMany({
      where: {
        organizationId: tenantId,
        OR: [
          { firstName: { contains: keyword, mode: 'insensitive' } },
          { lastName: { contains: keyword, mode: 'insensitive' } },
          { email: { contains: keyword, mode: 'insensitive' } },
        ],
      },
      take: limit,
    });
    return users.map(u => ({
      id: u.id,
      displayName: `${u.firstName} ${u.lastName}`,
      avatarUrl: u.avatarUrl,
    }));
  }
}

storageProvider (optional)

Implements IChatStorageProvider. File upload and deletion for media messages.

typescript
interface IChatStorageProvider {
  upload(file: Buffer | Readable, options: ChatUploadOptions): Promise<ChatUploadResult>;
  delete(fileUrl: string): Promise<void>;
  getSignedUrl?(fileUrl: string, expiresIn?: number): Promise<string>;
}

interface ChatUploadOptions {
  fileName: string;
  mimeType: string;
  folder?: string;
  tenantId: string;
}

interface ChatUploadResult {
  fileUrl: string;
  thumbnailUrl?: string;
  fileSize: number;
  mimeType: string;
}

Without a storage provider

If no storageProvider is provided, message endpoints still accept the fileUrl, fileName, etc. fields — but it is up to the client to handle the upload and provide the URL directly.

eventHandler (optional)

Implements IChatEventHandler. Asynchronous hooks triggered on chat events.

typescript
interface IChatEventHandler {
  onMessageSent?(channelId: string, message: Record<string, unknown>, tenantId: string): Promise<void>;
  onChannelCreated?(channel: Record<string, unknown>, tenantId: string): Promise<void>;
  onUserMentioned?(userId: string, channelId: string, messageId: string, tenantId: string): Promise<void>;
  onUnreadCountChanged?(userId: string, count: number, tenantId: string): Promise<void>;
}

Typical use cases:

  • Send push notifications when a message is received
  • Update a badge counter on the mobile app
  • Log to an external analytics system
  • Trigger a webhook to a third-party service
typescript
@Injectable()
class MyEventHandler implements IChatEventHandler {
  constructor(private notificationService: NotificationService) {}

  async onMessageSent(channelId: string, message: Record<string, unknown>, tenantId: string) {
    await this.notificationService.sendPush({
      channelId,
      senderId: message.senderId as string,
      text: message.text as string,
      tenantId,
    });
  }

  async onUserMentioned(userId: string, channelId: string, messageId: string, tenantId: string) {
    await this.notificationService.sendMentionNotification(userId, channelId, messageId);
  }
}

Environment variables

The SDK itself reads zero environment variables. Everything is passed explicitly through ChatModuleOptions. The variable names below are just a convention used in examples — nothing magical happens if you rename them.

You typically bridge env → ChatModuleOptions through NestJS's ConfigModule:

ts
ChatModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    database: { url: config.get('CHAT_DATABASE_URL')! },
    redis: { url: config.get('REDIS_URL')! },
    logging: { level: config.get('NODE_ENV') === 'production' ? 'info' : 'debug' },
  }),
  providers: {
    authGuard: ChatAuthGuard,
    userExtractor: ChatUserExtractor,
    userResolver: ChatUserResolver,
  },
}),

A typical .env for the chat integration:

ini
# Dedicated Postgres database (or a separate ?schema= on the same cluster) for chat data
CHAT_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/chat?schema=public"

# Redis: used for BullMQ (scheduled messages) and the Socket.IO adapter
REDIS_URL="redis://localhost:6379"

Anything else (JWT secrets, CORS origins, port, Socket.IO path) is your host app's concern — the SDK doesn't touch them.


CORS Configuration

CORS is handled entirely by your host application. nestjs-chat does not touch CORS — configure it in your NestJS bootstrap:

typescript
// Your host app configures its own NestJS CORS
app.enableCors({
  origin: ['https://app.example.com'],
  credentials: true,
});

CORS in production

Never leave origin: '*' in production. Explicitly list the allowed origins.


Socket.IO Configuration

Namespace

By default, the gateway listens on the /chat namespace. Clients connect to:

wss://api.example.com/chat

Rooms

The service uses Socket.IO rooms to target emissions:

RoomFormatUsage
Tenanttenant:{tenantId}Broadcast to the entire tenant
Channelchannel:{channelId}Messages, typing, reactions for a channel
Useruser:{userId}Personal events (notifications, unread count)

Client-to-Server Events

EventPayloadDescription
chat:typing:start{ channelId: string }Typing started
chat:typing:stop{ channelId: string }Typing stopped
chat:join:channel{ channelId: string }Join a channel's room
chat:leave:channel{ channelId: string }Leave a channel's room

Server-to-Client Events

38 events covering messages, channels, polls, and moderation. All events are prefixed with chat: and emitted in the relevant channel's room.

Examples:

EventEmitted when
chat:message:sentNew message in a channel
chat:message:updatedMessage edited
chat:message:deletedMessage deleted
chat:channel:createdNew channel created
chat:channel:updatedChannel modified (name, cover...)
chat:member:joinedNew member in a channel
chat:member:leftMember leaves a channel
chat:typingA user is typing in a channel
chat:reaction:addedReaction added to a message
chat:poll:votedVote on a poll
chat:channel:frozenChannel frozen by an operator
chat:user:mutedUser muted in a channel
chat:user:bannedUser banned from a channel

Complete integration example

A full working pattern combining forRootAsync with all five provider hooks:

typescript
@Module({
  imports: [
    ChatModule.forRootAsync({
      imports: [ConfigModule, AuthModule, UserModule, StorageModule],
      useFactory: (config: ConfigService) => ({
        database: { url: config.get('CHAT_DATABASE_URL') },
        redis: { url: config.get('REDIS_URL') },
        limits: {
          maxChannelMembers: 200,
          maxPinnedMessages: 10,
          maxMessageLength: 10000,
        },
      }),
      inject: [ConfigService],
      providers: {
        authGuard: JwtAuthGuard,
        userExtractor: JwtUserExtractor,
        userResolver: PrismaUserResolver,
        storageProvider: S3StorageProvider,
        eventHandler: PushNotificationHandler,
      },
    }),
  ],
})
export class AppModule {}

MIT License