Skip to content

HARRIFIED/Anon-chat-backend

Repository files navigation

Anonymous Chat Backend

A real-time anonymous chat backend built with NestJS, TypeORM (PostgreSQL), Redis, and Socket.IO. This system provides ephemeral chat rooms with advanced features like presence tracking, polls, mentions, message pinning, and comprehensive moderation tools.

Table of Contents


Overview

This backend powers anonymous chat rooms where:

  • Users can create temporary or persistent chat rooms
  • Each room has a unique invite token (and optional moderator token)
  • Messages can be ephemeral (auto-deleted based on retention policy)
  • Real-time presence tracking shows who's online
  • Advanced features: polls, mentions, replies, message pinning
  • Comprehensive moderation: kick/ban users, close rooms
  • Rate limiting and fingerprint-based abuse prevention

Architecture & Tech Stack

Core Technologies

  • NestJS - Modern TypeScript framework with modular architecture
  • TypeORM - ORM for PostgreSQL with migration support
  • PostgreSQL - Primary database for persistent data
  • Redis - Session storage, presence tracking, pub/sub messaging
  • Socket.IO - Real-time WebSocket communication
  • ioredis - Redis client library

Module Structure

src/
├── app.module.ts          # Root module
├── main.ts                # Bootstrap with CORS, validation
├── typeorm.config.ts      # Database configuration
├── rooms/                 # Room creation, validation, management
├── participants/          # Participant tracking, bans
├── auth/                  # Session & token management
├── messages/              # Messages, polls, notifications
├── websocket/             # Socket.IO gateway
├── redis/                 # Redis client service
├── analytics/             # Usage metrics
├── health/                # Health checks
└── common/                # Shared utilities, decorators, rate limiting

Key modules:

  • RoomsModule: Room lifecycle (create, join, close, end)
  • AuthModule: Session creation and validation
  • WebsocketModule: Real-time chat gateway
  • MessagesModule: Message persistence, polls, notifications
  • ParticipantsModule: Participant records, ban management
  • RedisModule: Redis client and subscriber instances
  • AnalyticsModule: Metrics tracking

Installation & Setup

Prerequisites

  • Node.js 18+
  • PostgreSQL 15+
  • Redis 7+
  • npm or yarn

Local Development

  1. Install dependencies

    npm install
  2. Set up environment variables Create a .env file in the root directory:

    # Database
    DATABASE_HOST=localhost
    DATABASE_PORT=5432
    DATABASE_USER=postgres
    DATABASE_PASS=postgres
    DATABASE_NAME=anonchat
    
    # Redis
    REDIS_HOST=localhost
    REDIS_PORT=6379
    REDIS_PASSWORD=
    
    # Application
    PORT=5000
    NODE_ENV=development
    FRONTEND_URL=http://localhost:3000
    
    # Session
    SESSION_TTL_SECONDS=604800  # 7 days
  3. Run database migrations

    npm run migration:run
  4. Start development server

    npm run start:dev

    Server runs on http://localhost:5000

Docker Compose

docker-compose up --build

This starts:

  • Backend service (port 5000)
  • PostgreSQL (port 5432)
  • Redis (port 6379)

Environment Variables

Variable Description Default
DATABASE_HOST PostgreSQL host localhost
DATABASE_PORT PostgreSQL port 5432
DATABASE_USER Database username postgres
DATABASE_PASS Database password postgres
DATABASE_NAME Database name anonchat
REDIS_HOST Redis host localhost
REDIS_PORT Redis port 6379
REDIS_PASSWORD Redis password (empty)
PORT Application port 5000
NODE_ENV Environment development
FRONTEND_URL Frontend origin for CORS (required)
SESSION_TTL_SECONDS Session expiry time 604800 (7 days)

Database Schema

Tables

rooms

{
  id: string (uuid, PK)
  title: string
  description?: string
  inviteTokenHash: string          // bcrypt hash
  moderatorTokenHash?: string      // bcrypt hash
  isOpen: boolean                  // Room open for new joins
  ephemeral: boolean               // Messages auto-delete
  retentionSeconds: number         // How long messages persist
  chatDurationSeconds: number      // Room auto-close timer
  maxParticipants: number
  actualParticipants: number
  settings: json                   // Additional config
  creatorIP?: string
  creatorAgent?: string
  createdAt: timestamp
  closedAt?: timestamp
}

participants

{
  id: string (uuid, PK)
  roomId: string (uuid)
  username: string
  anonymousId: string              // 6-char hex for privacy
  sessionId?: string
  avatar?: string
  joinedAt: timestamp
  leftAt?: timestamp
  isModerator: boolean
}

messages

{
  id: string (uuid, PK)
  roomId: string (uuid)
  authorUsername: string
  anonymousId?: string
  authorAvatar?: string
  authorSessionId?: string
  authorRole?: string
  clientMsgId?: string             // For client-side dedup
  content: json                    // { text?: string, type: 'text' | 'poll' }
  replyToId?: string (uuid)        // Parent message
  mentions?: string[]              // Mentioned usernames
  pinned: boolean
  pinnedAt?: timestamp
  pinnedBy?: string
  ip?: string
  userAgent?: string
  createdAt: timestamp
}

polls

{
  id: string (uuid, PK)
  roomId: string (uuid)
  messageId: string (uuid)         // Associated message
  question: string
  options: json[]                  // [{ id, text }]
  allowMultipleAnswers: boolean
  isClosed: boolean
  closedAt?: timestamp
  creatorUsername: string
  creatorAnonymousId?: string
  creatorSessionId?: string
  createdAt: timestamp
}

poll_votes

{
  id: string (uuid, PK)
  pollId: string (uuid)
  optionId: string                 // References poll.options[].id
  sessionId: string                // Voter session
  username: string
  anonymousId: string
  createdAt: timestamp
}

room_bans

{
  id: string (uuid, PK)
  roomId: string (uuid)
  fingerprintHash?: string
  deviceId?: string
  ip?: string
  userAgent?: string
  bannedBy: string                 // Moderator username
  reason?: string
  createdAt: timestamp
  expiresAt?: timestamp
}

room_fingerprints

Used to limit room creation per fingerprint.

notifications

{
  id: string (uuid, PK)
  roomId: string (uuid)
  recipientUsername: string
  recipientSessionId: string
  type: string                     // 'mention' | 'reply' | 'poll_vote' | etc.
  content: json
  messageId?: string (uuid)
  read: boolean
  createdAt: timestamp
}

Redis Data Structures

Redis is used for session storage, presence tracking, and pub/sub messaging.

Key Patterns

Sessions

session:<sessionId>              → JSON { sessionId, roomId, username, anonymousId, avatar, role, createdAt, ... }
session_token:<token>            → sessionId
username_session:<roomId>:<username> → sessionId
  • TTL: SESSION_TTL_SECONDS (default 7 days)
  • Sessions map tokens to user state
  • Username→session mapping prevents hijacking

Presence

presence:<roomId>                → Hash { sessionId: JSON { username, anonymousId, role, ... } }
  • Stores currently connected users per room
  • Updated on join/leave via WebSocket

Username Locks

room:<roomId>:username:<username> → "1"
  • TTL: SESSION_TTL_SECONDS
  • Ensures usernames are unique per room
  • Created on join, checked on connect

Pub/Sub Channels

room:<roomId>:messages           → Published messages broadcast to all connected clients
  • Gateway subscribes with pattern room:*:messages
  • Enables horizontal scaling (multiple backend instances)

REST API Endpoints

All endpoints are prefixed with /api.

Room Management

POST /api/rooms

Create a new room.

Request Body:

{
  "title": "My Chat Room",
  "description": "Optional description",
  "ephemeral": true,
  "chatDurationSeconds": 3600,
  "retentionSeconds": 2592000,
  "maxParticipants": 100,
  "fingerprintHash": "abc123...",
  "deviceId": "device-uuid"
}

Response:

{
  "roomId": "uuid",
  "inviteToken": "plain-token",
  "moderatorToken": "plain-moderator-token"
}

Rate Limit: 5 rooms per fingerprint/IP per hour


GET /api/rooms/:id

Get room metadata.

Response:

{
  "roomId": "uuid",
  "title": "Room Title",
  "description": "Description",
  "ephemeral": true,
  "createdAt": "ISO-8601",
  "isOpen": true,
  "actualParticipants": 5,
  "closedAt": null
}

POST /api/rooms/:id/validate-invite

Validate invite token before join.

Request Body:

{
  "inviteToken": "plain-token"
}

Response:

{
  "ok": true
}

POST /api/rooms/:id/join

Join a room and create a session.

Request Body:

{
  "inviteToken": "plain-token",
  "moderatorToken": "optional-mod-token",
  "username": "Alice",
  "avatar": "optional-avatar-url",
  "fingerprintHash": "fingerprint-hash",
  "deviceId": "device-uuid"
}

Response:

{
  "sessionToken": "hex-token",
  "sessionId": "uuid",
  "anonymousId": "6-char-hex",
  "role": "participant" | "moderator"
}

Notes:

  • Checks if username is taken
  • Checks if user is banned
  • Creates session in Redis
  • Locks username
  • Records participant in database

POST /api/rooms/:id/leave

Leave a room (cleanup session, presence).

Headers: Authorization: Bearer <sessionToken>

Response: 200 OK


POST /api/rooms/:id/close

Close room to new participants (moderator only).

Headers: Authorization: Bearer <sessionToken>

Response: 200 OK


POST /api/rooms/:id/end

End the room permanently (moderator only).

Headers: Authorization: Bearer <sessionToken>

Response: 200 OK


POST /api/rooms/:id/kick

Kick a user from the room (moderator only).

Headers: Authorization: Bearer <sessionToken>

Request Body:

{
  "targetSessionId": "uuid"
}

Response: 200 OK


POST /api/rooms/:id/ban

Ban a user from the room (moderator only).

Headers: Authorization: Bearer <sessionToken>

Request Body:

{
  "targetSessionId": "uuid",
  "reason": "Optional reason"
}

Response: 200 OK


GET /api/rooms/:id/participants

Get active participants.

Response:

{
  "participants": [
    {
      "sessionId": "uuid",
      "username": "Alice",
      "anonymousId": "abc123",
      "role": "participant",
      "avatar": "url"
    }
  ]
}

GET /api/rooms/:id/remaining-time

Get room time remaining before auto-close.

Response:

{
  "remainingSeconds": 3420
}

WebSocket Events

WebSocket namespace: / (root)
Connect with query param: ?t=<sessionToken>

Client → Server Events

identify

Authenticate after connecting without token.

Payload:

{
  "token": "session-token"
}

Response Events:

  • identified - Authentication successful
  • error - Authentication failed

send_message

Send a chat message.

Payload:

{
  "text": "Hello world",
  "clientMsgId": "client-uuid",
  "replyToId": "optional-message-uuid"
}

Response Events:

  • message_ack - Message saved, broadcasted
  • error - Message failed

Rate Limit: 10 messages per 10 seconds per session


typing

Indicate user is typing.

Payload:

{
  "typing": true
}

Broadcast Event: typing to room with { username, anonymousId, typing }


create_poll

Create a poll (moderator only).

Payload:

{
  "question": "What's your favorite color?",
  "options": ["Red", "Blue", "Green"],
  "allowMultipleAnswers": false
}

Broadcast Event: message with poll message


vote_poll

Vote on a poll.

Payload:

{
  "pollId": "uuid",
  "optionId": "option-id"
}

Response Events:

  • vote_recorded - Vote saved
  • poll_update - Updated poll results broadcast to room

remove_vote

Remove your vote from a poll.

Payload:

{
  "pollId": "uuid",
  "optionId": "option-id"
}

Response Events:

  • vote_removed
  • poll_update

close_poll

Close a poll (moderator only).

Payload:

{
  "pollId": "uuid"
}

Broadcast Event: poll_closed


get_poll_results

Get detailed poll results.

Payload:

{
  "pollId": "uuid"
}

Response Event: poll_results


pin_message

Pin a message (moderator only).

Payload:

{
  "messageId": "uuid"
}

Broadcast Event: message_pinned


unpin_message

Unpin a message (moderator only).

Payload:

{
  "messageId": "uuid"
}

Broadcast Event: message_unpinned


get_pinned_messages

Get all pinned messages in room.

Response Event: pinned_messages


get_notifications

Get unread notifications for user.

Response Event: notifications


mark_notification_read

Mark a notification as read.

Payload:

{
  "notificationId": "uuid"
}

mark_all_notifications_read

Mark all notifications as read.


Server → Client Events

identified

Sent after successful authentication.

Payload:

{
  "sessionId": "uuid",
  "roomId": "uuid",
  "username": "Alice",
  "role": "participant"
}

presence_state

Full presence snapshot sent on connect.

Payload:

{
  "users": [
    {
      "sessionId": "uuid",
      "username": "Alice",
      "anonymousId": "abc123",
      "role": "participant",
      "avatar": "url"
    }
  ]
}

presence

User joined or left.

Payload:

{
  "event": "join" | "leave",
  "user": {
    "sessionId": "uuid",
    "username": "Alice",
    "anonymousId": "abc123"
  }
}

presence_removed

Specific user was removed (kicked/banned).

Payload:

{
  "sessionId": "uuid"
}

history_chunk

Message history sent on connect.

Payload:

{
  "messages": [ /* array of message objects */ ]
}

message

New message broadcast to room.

Payload:

{
  "id": "uuid",
  "roomId": "uuid",
  "authorUsername": "Alice",
  "anonymousId": "abc123",
  "content": { "text": "Hello" },
  "createdAt": "ISO-8601",
  "replyToId": "optional-uuid",
  "mentions": ["Bob", "Charlie"]
}

message_ack

Acknowledgement of sent message.

Payload:

{
  "clientMsgId": "client-uuid",
  "serverMessageId": "server-uuid"
}

typing

User typing indicator.

Payload:

{
  "username": "Alice",
  "anonymousId": "abc123",
  "typing": true
}

poll_update

Poll vote counts updated.

room_closed

Room closed by moderator.

kicked

You were kicked from the room.

error

Error message.

Payload:

{
  "message": "Error description"
}

System Flows

Room Creation Flow

  1. Client sends POST /api/rooms with room details and fingerprint
  2. Backend checks rate limit (5 rooms/hour per fingerprint)
  3. Backend generates:
    • Room UUID
    • Invite token (plain) + bcrypt hash
    • Moderator token (plain) + bcrypt hash
  4. Backend stores room in PostgreSQL
  5. Backend records fingerprint association
  6. Backend returns { roomId, inviteToken, moderatorToken }

Joining a Room Flow

  1. Client optionally validates invite via POST /api/rooms/:id/validate-invite
  2. Client sends POST /api/rooms/:id/join with:
    • inviteToken or moderatorToken
    • username
    • fingerprintHash, deviceId
  3. Backend validates token (bcrypt compare)
  4. Backend checks:
    • Is room open?
    • Is username available?
    • Is user banned?
    • Has room hit max participants?
  5. Backend creates session in Redis:
    • Generates sessionToken, sessionId, anonymousId
    • Sets role: moderator if moderator token provided, else participant
    • Maps session:<sessionId>, session_token:<token>, username_session:<roomId>:<username>
  6. Backend sets username lock: room:<roomId>:username:<username>
  7. Backend destroys any old sessions for this username
  8. Backend records participant in database
  9. Backend returns { sessionToken, sessionId, anonymousId, role }

WebSocket Connection Flow

  1. Client connects via Socket.IO with ?t=<sessionToken> or sends identify event
  2. Backend retrieves session from Redis via session_token:<token>
  3. Backend validates:
    • Session exists?
    • Username lock still held?
    • No conflicting session using same username?
  4. Backend checks for duplicate connection (within 2 seconds) to prevent double presence
  5. Backend attaches session data to socket: client.data.session
  6. Backend joins socket to room: room_<roomId>
  7. Backend adds to presence hash: presence:<roomId>
  8. Backend sends to client:
    • identified event
    • presence_state (full user list)
    • history_chunk (recent messages)
    • pinned_messages
  9. Backend broadcasts presence event (join) to room

Message Sending Flow

  1. Client sends send_message event with { text, clientMsgId, replyToId? }
  2. Backend checks rate limit (10 messages / 10 seconds)
  3. Backend extracts mentions from text (e.g., @Alice)
  4. Backend saves message to PostgreSQL messages table
  5. Backend publishes message to Redis: PUBLISH room:<roomId>:messages <json>
  6. Backend creates notifications for mentioned users
  7. Backend sends message_ack to sender
  8. Redis subscriber receives published message
  9. Backend broadcasts message to all clients in room_<roomId>

Session Management

Sessions are stateless tokens stored in Redis.

Session Creation

  • On room join, SessionService.createSession() generates:
    • sessionId (UUIDv4)
    • sessionToken (64-char hex)
    • anonymousId (6-char hex, for privacy)
  • Session payload includes: roomId, username, role, avatar, ip, userAgent, fingerprintHash, deviceId
  • TTL: SESSION_TTL_SECONDS (default 604800 = 7 days)

Session Validation

  • On WebSocket connect, token is validated via getSessionByToken()
  • Username lock key is checked: room:<roomId>:username:<username>
  • If lock expired, session is destroyed and connection rejected

Anti-Hijack Logic

  • destroyOldSessionsForUsername() removes any existing session with same username before creating new one
  • Prevents username theft when someone reconnects
  • Cleans up old session keys, tokens, username mappings, and presence entries

Session Cleanup

  • On leave/disconnect: destroySession() or destroySessionById()
  • Removes: session keys, token keys, username mappings, presence hash entries
  • Username lock expires naturally via TTL

Presence System

Presence tracks who's currently connected to each room.

Data Structure

  • Redis Hash: presence:<roomId>
    • Key: sessionId
    • Value: JSON { username, anonymousId, role, avatar }

Lifecycle

  1. Join: Added to hash on WebSocket connect, presence event broadcast
  2. Reconnect: Duplicate check prevents double-emission
  3. Leave: Removed from hash on disconnect, presence event broadcast
  4. Kick/Ban: Removed + presence_removed event broadcast

In-Memory Tracking

  • ChatGateway.activeConnections Map tracks recent connections
  • Prevents duplicate presence events when client reconnects quickly (<2s)

Features

1. Messages

  • Text messages with @mentions
  • Message replies (threading)
  • Ephemeral messages (auto-delete based on room retention)
  • Message acknowledgments (clientMsgId → serverMessageId)

2. Polls

  • Create polls with multiple options
  • Single or multiple answer modes
  • Vote/unvote
  • Real-time vote count updates
  • Close poll (moderator only)
  • View detailed results

3. Notifications

  • Mention notifications
  • Reply notifications
  • Poll vote notifications
  • Persistent in database
  • Mark as read

4. Message Pinning

  • Moderators can pin important messages
  • Pinned messages sent to new joiners
  • Unpin support

5. Moderation

  • Kick users (disconnect + remove presence)
  • Ban users (prevents rejoin via fingerprint/IP/deviceId)
  • Close room (prevent new joins)
  • End room (permanent shutdown)

6. Analytics

  • Track: sessions, messages, rooms created, failed auths
  • Exposed via /api/analytics endpoints

Security & Rate Limiting

Rate Limiting

Implemented via RateLimitService (Redis-backed).

  • Room Creation: 5 rooms per fingerprint/IP per hour
  • Message Send: 10 messages per 10 seconds per session
  • Join Room: 5 join attempts per fingerprint per 5 minutes

Ban System

  • Multi-factor banning: fingerprintHash, deviceId, ip, userAgent
  • Persistent in room_bans table
  • Checked on join and connect

Fingerprinting

  • Client-side fingerprinting (browser fingerprint)
  • Sent as fingerprintHash in requests
  • Used for rate limiting and ban enforcement

Input Validation

  • Global ValidationPipe with whitelist, forbidNonWhitelisted, transform
  • DTOs with class-validator decorators

CORS

  • Configurable allowed origins via FRONTEND_URL and hardcoded domains
  • Credentials enabled for session cookies (if used)

Development Workflow

Running the App

# Development with watch mode
npm run start:dev

# Production build
npm run build
npm run start:prod

Database Migrations

# Generate migration from entity changes
npm run migration:generate

# Run pending migrations
npm run migration:run

# Revert last migration
npm run migration:revert

# Production migrations
npm run migration:run:prod
npm run migration:revert:prod

Testing

# Unit tests
npm run test

# Watch mode
npm run test:watch

# E2E tests
npm run test:e2e

# Coverage
npm run test:cov

Docker

# Start all services
docker-compose up --build

# Stop services
docker-compose down

# View logs
docker-compose logs -f backend

Project Structure Details

Key Files

  • src/main.ts - Bootstrap, CORS, global validation pipe
  • src/app.module.ts - Root module with TypeORM, Redis, all feature modules
  • src/typeorm.config.ts - TypeORM CLI config for migrations
  • src/redis/redis.service.ts - Redis client and subscriber instances
  • src/websocket/chat.gateway.ts - Socket.IO gateway (all realtime logic)
  • src/auth/session.service.ts - Session CRUD and anti-hijack logic
  • src/rooms/rooms.service.ts - Room business logic
  • src/messages/messages.service.ts - Message persistence and retrieval
  • src/messages/polls.service.ts - Poll operations

Important Patterns

  • Redis Key Prefixes: Consistent naming (session:, presence:, room:)
  • Socket Rooms: Named room_<roomId> (underscore prefix)
  • Pub/Sub: Pattern room:*:messages for message broadcasting
  • Presence: Hash key stores JSON, updated on join/leave
  • Session TTL: All session-related keys share same TTL
  • Logging: Nest Logger with context strings

API Base URL

All endpoints are prefixed with /api (configured via app.setGlobalPrefix('api') in main.ts).

Example: http://localhost:5000/api/rooms


WebSocket Connection

Connect to: ws://localhost:5000/?t=<sessionToken>

Or connect without token and send identify event with { token: "..." }.


License

UNLICENSED


Author

Harrison Ikpefua


For more details on specific features like polls implementation, see POLLS_IMPLEMENTATION.md.

About

A real-time anonymous chat backend built with NestJS, TypeORM (PostgreSQL), Redis, and Socket.IO. This system provides ephemeral chat rooms with advanced features like presence tracking, polls, mentions, message pinning, and comprehensive moderation tools.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors