Skip to content

ashusevim/websocket

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

86 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WebSocket Chat Application

Real-time chat with authentication, built using Express, PostgreSQL, WebSockets, and JWT tokens.

Table of Contents

Features

  • Authentication: User registration/login with bcrypt password hashing and JWT tokens
  • Real-time Chat: WebSocket-based messaging with automatic reconnection
  • Security: Rate limiting, input sanitization, origin validation, token-based auth
  • Monitoring: Winston logging and Sentry error tracking
  • Modern UI: Responsive dark/light theme, connection status, user list
  • Graceful Shutdown: Ensures all connections are closed properly before the server exits
  • Production Ready: Docker containerization, health checks, environment-based config

Quick Start

# 1. Database setup
createdb websocket_chat
psql -U postgres -d websocket_chat << EOF
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE active_tokens (
    id SERIAL PRIMARY KEY,
    token VARCHAR(64) UNIQUE NOT NULL,
    username VARCHAR(50) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (username) REFERENCES users(username) ON DELETE CASCADE
);
EOF

# 2. Configure environment
cd server
cat > .env << EOF
DB_USER=postgres
DB_PASSWORD=your_password
DB_HOST=localhost
DB_PORT=5432
DB_NAME=websocket_chat
PORT=8080
JWT_SECRET=$(openssl rand -hex 32)
NODE_ENV=development
EOF

# 3. Install & run server
npm ci
npm run dev:hot

# 4. Serve client (new terminal)
npx serve client -l 5500

# 5. Open http://localhost:5500

Project Structure

websocket/
├── server/
│   ├── src/
│   │   ├── index.ts          # Main server (WebSocket + auth endpoints)
│   │   ├── db.ts             # PostgreSQL connection pool
│   │   ├── logger.ts         # Winston logger configuration
│   │   ├── instrument.ts     # Sentry setup
│   │   └── utils/
│   │       ├── validation.ts # Input validation functions
│   │       └── sanitize.ts   # XSS prevention utilities
│   ├── Dockerfile
│   ├── package.json
│   └── tsconfig.json
├── client/
│   ├── index.html            # Chat UI with login/register
│   └── styles.css
├── render.yaml               # Render deployment config
└── README.md

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                         CLIENT (Browser)                        │
│  ┌─────────────────┐    ┌─────────────────────────────────────┐ │
│  │  Login/Register │    │         Chat Interface              │ │
│  │      Forms      │    │  - Message input                    │ │
│  │                 │    │  - User list                        │ │
│  │                 │    │  - Real-time messages               │ │
│  └────────┬────────┘    └──────────────┬──────────────────────┘ │
└───────────┼────────────────────────────┼────────────────────────┘
            │ HTTP (REST API)            │ WebSocket (wss://)
            ▼                            ▼
┌─────────────────────────────────────────────────────────────────┐
│                       EXPRESS SERVER                             │
│  ┌──────────────────┐  ┌──────────────────┐  ┌───────────────┐  │
│  │   REST Endpoints │  │  WebSocket Server │  │  Middleware   │  │
│  │  /register       │  │  - Connection     │  │  - CORS       │  │
│  │  /login          │  │  - Message        │  │  - Rate Limit │  │
│  │  /logout         │  │  - Broadcast      │  │  - JSON Parse │  │
│  │  /health         │  │  - User List      │  │               │  │
│  └────────┬─────────┘  └────────┬─────────┘  └───────────────┘  │
└───────────┼─────────────────────┼───────────────────────────────┘
            │                     │
            ▼                     ▼
┌─────────────────────────────────────────────────────────────────┐
│                        POSTGRESQL                                │
│  ┌─────────────────────┐    ┌─────────────────────────────────┐ │
│  │      users          │    │        active_tokens            │ │
│  │  - id               │    │  - id                           │ │
│  │  - username         │    │  - token                        │ │
│  │  - password_hash    │    │  - username (FK)                │ │
│  │  - created_at       │    │  - created_at                   │ │
│  └─────────────────────┘    └─────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

Data Flow

  1. Registration/Login: Client sends credentials via HTTP POST
  2. Token Generation: Server creates JWT, stores in database
  3. WebSocket Connection: Client connects with token in query string
  4. Token Verification: Server validates JWT and checks database
  5. Real-time Messaging: Bidirectional communication over WebSocket
  6. Logout: Token removed from database, invalidating the session

API Reference

POST /register

// Request
{ "username": "alice", "password": "secret123" }

// Response (201)
{ "message": "User created successfully", "username": "alice" }

POST /login

// Request
{ "username": "alice", "password": "secret123" }

// Response (200)
{ "token": "jwt-token-string", "username": "alice" }

POST /logout

// Request
{ "token": "jwt-token-string" }

// Response (200)
{ "message": "User logout successfully" }

GET /health

// Response (200)
{ "status": "ok", "timeStamp": "2025-10-14T12:34:56.789Z" }

GET /debug-sentry

Triggers a test error to verify Sentry integration.

WebSocket Protocol

Connection: ws://localhost:8080?token=<jwt-token>

Client → Server

{ "type": "chat", "message": "Hello!" }

Server → Client

// Chat message
{ "username": "alice", "message": "Hello, "timestamp": "12:34:56 PM" }

// System announcement
{ "type": "announcement", "message": "alice has joined the chat room" }

// User list update
{ "type": "userList", "users": ["alice", "bob"] }

Message Types

// Server → Client
type ServerMessage = 
    | { type: "message"; username: string; message: string; timestamp: string }
    | { type: "userList"; users: string[] }
    | { type: "announcement"; message: string }
    | { type: "error"; message: string };

// Client → Server
type ClientMessage = 
    | { type: "message"; message: string };

Configuration

Environment Variables

# Database
DB_USER=postgres
DB_PASSWORD=your_password
DB_HOST=localhost
DB_PORT=5432
DB_NAME=websocket_chat

# Server
PORT=8080
NODE_ENV=development

# Auth
JWT_SECRET=your-secret-key  # Generate with: openssl rand -hex 32

# Monitoring (optional)
SENTRY_DSN=your-sentry-dsn

Allowed Origins

Edit allowedOrigins in server/src/index.ts:

const allowedOrigins = [
    "http://127.0.0.1:5500",
    "http://localhost:8080",
    "http://localhost:5500",
    "ws://localhost:8080",
    "https://websocket-chat-server-ptfw.onrender.com",
    "wss://websocket-chat-server-ptfw.onrender.com",
    "https://websocket-chat-client-ptfw.onrender.com",
    "wss://websocket-chat-client-ptfw.onrender.com"
];

Development Scripts

npm run dev:hot        # Hot reload (TypeScript + Nodemon)
npm run dev:watch      # Watch TypeScript compilation only
npm run start:watch    # Watch and restart server only
npm run build          # Compile TypeScript to JavaScript
npm run start          # Run the compiled JavaScript server

Security

Security Features Overview

Feature Implementation Purpose
Password Hashing bcrypt (10 rounds) Protect stored passwords
Input Validation Regex patterns Prevent malformed data
XSS Prevention HTML entity encoding Prevent script injection
Rate Limiting 200 req/hr, 20 msg/10s Prevent DoS/spam
Origin Validation Whitelist check Prevent unauthorized access
SQL Injection Parameterized queries Protect database
Token Invalidation Database-backed logout Revoke compromised sessions

Password Hashing

// Registration - hash password
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);

// Login - verify password
const match = await bcrypt.compare(password, user.password_hash);

Why 10 salt rounds? Takes ~100ms to hash, balancing security with UX.

Input Sanitization (XSS Prevention)

export function sanitize(str: string): string {
    return str
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#39");
}

Input Validation

export function isValidUsername(username: string): boolean {
    return username.length >= 3 && 
           username.length <= 50 && 
           /^[a-zA-Z0-9_]+$/.test(username);
}

export function isValidPassword(password: string): boolean {
    return password.length >= 8 && password.length <= 128;
}

export function isValidMessage(message: string): boolean {
    return message.length >= 1 && message.length <= 500;
}

Rate Limiting

// HTTP rate limiting (Express middleware)
const limiter = rateLimit({
    windowMs: 60 * 60 * 1000, // 1 hour
    max: 200,                  // 200 requests per hour
    message: 'Too many requests, please try again later'
});

// WebSocket message rate limiting
const MESSAGE_LIMIT = 20;
const TIME_WINDOW = 10000; // 10 seconds

ws.on("message", () => {
    ws.messageCount = (ws.messageCount || 0) + 1;
    if (ws.messageCount > MESSAGE_LIMIT) {
        ws.close(1008, "Rate limit exceeded");
    }
});

SQL Injection Prevention

// ❌ VULNERABLE
// const query = \`SELECT * FROM users WHERE username = '\${username}'\`;

// ✅ SAFE - parameterized queries
const query = 'SELECT * FROM users WHERE username = $1';
const result = await pool.query(query, [username]);

Technical Deep Dive

WebSocket vs HTTP

Feature HTTP Polling WebSocket
Connection New per request Persistent
Latency High (polling interval) Low (<50ms)
Server Load High (constant requests) Low
Bidirectional No (client initiates) Yes
Overhead HTTP headers each time Minimal frame headers
// WebSocket: Persistent bidirectional connection
const wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
    ws.on("message", (msg) => { /* handle */ });
    ws.send(JSON.stringify({ type: "announcement", message: "..." }));
});

JWT Authentication Flow

┌──────────┐                              ┌──────────┐                    ┌──────────┐
│  Client  │                              │  Server  │                    │ Database │
└────┬─────┘                              └────┬─────┘                    └────┬─────┘
     │  1. POST /login (username, password)    │                               │
     │────────────────────────────────────────►│                               │
     │                                         │  2. Query user                │
     │                                         │──────────────────────────────►│
     │                                         │  3. Return user + hash        │
     │                                         │◄──────────────────────────────│
     │                                         │  4. bcrypt.compare()          │
     │                                         │  5. Generate JWT              │
     │                                         │  6. Store token in DB         │
     │                                         │──────────────────────────────►│
     │  7. Return { token }                    │                               │
     │◄────────────────────────────────────────│                               │
     │  8. Connect WebSocket with token        │                               │
     │────────────────────────────────────────►│                               │
     │                                         │  9. Verify JWT + check DB     │
     │  10. Connection established             │                               │
     │◄───────────────────────────────────────►│                               │

Hybrid JWT approach: Tokens stored in database enable logout invalidation while keeping JWT benefits.

Pure JWT Hybrid Approach
Cannot invalidate tokens Can invalidate on logout
Stateless Minimal state (active tokens only)
Fast verification Slight DB overhead
Vulnerable if stolen Token can be revoked

Graceful Shutdown

async function gracefulShutdown() {
    logger.info('Received shutdown signal...');
    
    server.close((err) => {           // 1. Stop accepting HTTP
        wss.close(() => {              // 2. Close WebSocket server
            Sentry.close(2000).then(() => {  // 3. Flush monitoring
                pool.end(() => {        // 4. Close database
                    process.exit(0);    // 5. Exit cleanly
                });
            });
        });
    });
    
    setTimeout(() => process.exit(1), 10000); // Force exit timeout
}

process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);

Real-time User Presence

interface ChatWebSocket extends WebSocket {
    username?: string;
    isAlive?: boolean;
    messageCount?: number;
}

function getConnectedUsers(): string[] {
    const users: string[] = [];
    wss.clients.forEach((client: ChatWebSocket) => {
        if (client.readyState === WebSocket.OPEN && client.username) {
            if (!users.includes(client.username)) {
                users.push(client.username);
            }
        }
    });
    return users;
}

function broadcastUserlist() {
    const connectedUsers = getConnectedUsers();
    wss.clients.forEach((client) => {
        if (client.readyState === WebSocket.OPEN) {
            client.send(JSON.stringify({ type: "userList", users: connectedUsers }));
        }
    });
}

Deployment

Docker Compose (Local)

version: '3.8'
services:
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: websocket_chat
      POSTGRES_PASSWORD: \${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

  app:
    build: .
    depends_on: [postgres]
    environment:
      DB_HOST: postgres
      JWT_SECRET: \${JWT_SECRET}
    ports:
      - "8080:8080"

volumes:
  postgres_data:
docker-compose up -d

Render.yaml (Production)

services:
  - type: web
    name: websocket-chat-server
    env: docker
    dockerfilePath: ./server/Dockerfile
    healthCheckPath: /health
    envVars:
      - key: DATABASE_URL
        fromDatabase:
          name: websocket-chat-db
          property: connectionString
      - key: JWT_SECRET
        generateValue: true
      - key: NODE_ENV
        value: production

  - type: web
    name: websocket-chat-client
    env: static
    staticPublishPath: ./client

databases:
  - name: websocket-chat-db
    plan: free

Dockerfile

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]

Scaling Strategy

                    ┌─────────────────┐
                    │  Load Balancer  │
                    │ (Sticky Sessions)│
                    └────────┬────────┘
         ┌───────────────────┼───────────────────┐
         ▼                   ▼                   ▼
   ┌──────────┐        ┌──────────┐        ┌──────────┐
   │ Server 1 │        │ Server 2 │        │ Server 3 │
   └────┬─────┘        └────┬─────┘        └────┬─────┘
        └───────────────────┼───────────────────┘
                    ┌───────▼───────┐
                    │  Redis Pub/Sub │
                    └───────┬───────┘
                    ┌───────▼───────┐
                    │   PostgreSQL   │
                    │ (+ Read Replicas)│
                    └────────────────┘

Key scaling changes:

  1. Redis Pub/Sub for cross-server message broadcasting
  2. Sticky sessions or shared session store
  3. Load balancer with WebSocket support (nginx)
  4. Message queues (RabbitMQ/Kafka) for guaranteed delivery
  5. Database read replicas for authentication queries

Troubleshooting

Can't connect to database?

sudo systemctl status postgresql
psql -U postgres -d websocket_chat -c "SELECT 1;"

WebSocket handshake fails (403)?

  • Use HTTP server, not `file://`
  • Check token is valid
  • Verify origin is in `allowedOrigins`

JWT_SECRET error?

# Add to .env
JWT_SECRET=\$(openssl rand -hex 32)

Rate limit hit?

  • Wait 10 seconds (20 messages per 10 seconds limit)
  • Client reconnects automatically

Technologies

Category Technology Why
Runtime Node.js Event-driven, perfect for WebSockets
Language TypeScript Type safety, compile-time errors
Framework Express 5 Mature, middleware ecosystem
WebSocket ws Lightweight, no Socket.io overhead
Database PostgreSQL ACID compliance, reliable
Auth JWT + bcrypt Stateless tokens, secure hashing
Logging Winston Log levels, environment-aware
Monitoring Sentry Error tracking, alerting
Deployment Docker + Render Reproducible, scalable

Metrics

Category Metric Value
Performance Message Latency <50ms
Performance Connection Time <100ms
Performance Memory Usage ~50MB
Security Password Hashing bcrypt 10 rounds
Security Token Expiry 1 hour
Quality Type Coverage 100% TypeScript

Checklist

  • ✅ Health check endpoint
  • ✅ Graceful shutdown
  • ✅ Environment-based config
  • ✅ Docker containerization
  • ✅ CORS configuration
  • ✅ Rate limiting
  • ✅ Error monitoring (Sentry)
  • ✅ Structured logging (Winston)

License

MIT

About

A real-time chat system with JWT-secured WebSocket sessions, rate limiting, and sanitized messaging using Express.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors