Back to Blog
Tutorial2026-08-067 min read

Build a 2026 Real-Time WebSocket Notification Engine with Bun.js, Elysia.js & PostgreSQL: A Scalable Tutorial for Indian Full-Stack Developers

Learn how to build a production-grade real-time notification engine using Bun.js, Elysia.js, and PostgreSQL — optimized for the Indian tech ecosystem and scalable to millions of concurrent connections.

Agentic Academy Labs

Author

Why Real-Time Notifications Matter in 2026

Real-time functionality is no longer a nice-to-have — it's the backbone of modern applications. Whether you're building a trading platform for Indian stock markets, a live order-tracking system for e-commerce, or a collaborative SaaS tool, WebSockets deliver the low-latency, bidirectional communication that users expect.

In this tutorial, we'll build a complete real-time notification engine using three tools that have reshaped the Node.js ecosystem in 2026:

  • Bun.js — The blazing-fast JavaScript runtime that's now the default choice for many Indian startups
  • Elysia.js — The lightweight, type-safe web framework that makes WebSocket integration effortless
  • PostgreSQL — The battle-tested database powering everything from UPI-based fintech apps to enterprise SaaS platforms

By the end, you'll have a fully functional notification system that can handle thousands of concurrent connections with sub-50ms latency.


Prerequisites

Before diving in, make sure you have the following installed:

  • Node.js 22+ or Bun 1.1+ (Bun is preferred for this tutorial)
  • PostgreSQL 16+ running locally or via a managed service
  • TypeScript knowledge (we'll write everything in TypeScript)
  • Basic understanding of WebSocket protocols

If you're new to Bun, check out Bun's official docs — the runtime has matured significantly in 2026 with native SQLite, edge runtime support, and a dramatically improved DX.


Step 1: Project Setup

Create a new directory and initialize your project with Bun:

mkdir notification-engine && cd notification-engine
bun init -y
tbun add elysia @elysiajs/websocket pg @types/pg
bun add -d typescript @types/bun

Create a tsconfig.json for strict TypeScript:

{
  "compilerOptions": {
    "target": "ES2023",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

Step 2: PostgreSQL Schema Design

A notification engine needs a robust database schema. Here's a production-ready design:

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  email VARCHAR(255) UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE notifications (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
  title VARCHAR(255) NOT NULL,
  body TEXT,
  type VARCHAR(50) DEFAULT 'info',
  is_read BOOLEAN DEFAULT FALSE,
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_notifications_user_id ON notifications(user_id);
CREATE INDEX idx_notifications_unread ON notifications(user_id, is_read);

The JSONB column for metadata is critical — it lets you store arbitrary payloads (order IDs, transaction references, deep links) without schema migrations.


Step 3: Building the Elysia.js WebSocket Server

Elysia.js makes WebSocket integration remarkably clean. Here's our server:

// src/server.ts
import { Elysia, t } from "elysia";
import { websocket } from "@elysiajs/websocket";
import { Pool } from "pg";

const pool = new Pool({
  connectionString: Bun.env.DATABASE_URL,
  max: 20,
});

// Track active WebSocket connections per user
const activeConnections = new Map<number, Set<WebSocket>>();

const app = new Elysia()
  .use(
    websocket({
      path: "/ws/notifications",
      open(ws) {
        console.log(`Client connected: ${ws.id}`);
      },
      message(ws, message) {
        const data = JSON.parse(message.toString());
        handleClientMessage(ws, data);
      },
      close(ws) {
        cleanupConnection(ws);
        console.log(`Client disconnected: ${ws.id}`);
      },
      // Handle ping/pong for connection health
      ping(ws) {
        ws.send(JSON.stringify({ type: "pong" }));
      },
    })
  )
  .get("/notifications/:userId", async ({ params }) => {
    const { userId } = params;
    const result = await pool.query(
      `SELECT * FROM notifications WHERE user_id = $1 AND is_read = FALSE ORDER BY created_at DESC LIMIT 50`,
      [userId]
    );
    return { notifications: result.rows };
  })
  .post("/notifications", async ({ body }) => {
    const { userId, title, body: text, type, metadata } = body;
    const result = await pool.query(
      `INSERT INTO notifications (user_id, title, body, type, metadata)
       VALUES ($1, $2, $3, $4, $5) RETURNING *`,
      [userId, title, text, type, JSON.stringify(metadata || {})]
    );

    // Broadcast to all connected WebSocket clients for this user
    broadcastToUser(userId, {
      type: "new_notification",
      payload: result.rows[0],
    });

    return { success: true, notification: result.rows[0] };
  })
  .listen(3000);

function handleClientMessage(ws: WebSocket, data: any) {
  if (data.type === "authenticate") {
    ws.userId = data.userId;
    if (!activeConnections.has(data.userId)) {
      activeConnections.set(data.userId, new Set());
    }
    activeConnections.get(data.userId)!.add(ws);
  }
}

function cleanupConnection(ws: WebSocket) {
  for (const [userId, connections] of activeConnections.entries()) {
    connections.delete(ws);
    if (connections.size === 0) {
      activeConnections.delete(userId);
    }
  }
}

function broadcastToUser(userId: number, message: any) {
  const connections = activeConnections.get(userId);
  if (connections) {
    const payload = JSON.stringify(message);
    connections.forEach((ws) => {
      if (ws.readyState === 1) {
        ws.send(payload);
      }
    });
  }
}

console.log(`🚀 Notification engine running on port 3000`);

Step 4: Understanding the Architecture

Here's what makes this architecture powerful in 2026:

  • Bun's native WebSocket support eliminates the need for external libraries like ws or socket.io — Bun handles the protocol natively with zero overhead
  • Elysia's type-safe routing means your API contracts are validated at compile time, catching bugs before deployment
  • PostgreSQL's JSONB gives you schema flexibility for notification payloads without sacrificing query performance
  • The activeConnections Map provides O(1) lookup for broadcasting, which is critical at scale

Step 5: Client-Side Integration

Here's how a frontend client connects and listens for notifications:

// client.ts
class NotificationClient {
  private ws: WebSocket | null = null;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 10;

  connect(userId: number, token: string) {
    this.ws = new WebSocket(`ws://localhost:3000/ws/notifications`);

    this.ws.onopen = () => {
      this.reconnectAttempts = 0;
      this.ws?.send(JSON.stringify({ type: "authenticate", userId }));
    };

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data.toString());
      this.handleMessage(data);
    };

    this.ws.onclose = () => {
      if (this.reconnectAttempts < this.maxReconnectAttempts) {
        const delay = Math.min(1000 * 2 ** this.reconnectAttempts, 30000);
        setTimeout(() => {
          this.reconnectAttempts++;
          this.connect(userId, token);
        }, delay);
      }
    };
  }

  private handleMessage(data: any) {
    switch (data.type) {
      case "new_notification":
        this.showToast(data.payload);
        break;
      case "pong":
        break;
    }
  }

  private showToast(notification: any) {
    // Integrate with your UI framework of choice
    console.log(`🔔 ${notification.title}: ${notification.body}`);
  }
}

The exponential backoff reconnection strategy ensures resilience — a critical pattern for production systems where network interruptions are common, especially on mobile networks across India's diverse connectivity landscape.


Scaling Strategies for Production in 2026

A single-server setup works for prototyping, but here's how to scale for production:

  1. Horizontal Scaling with Redis Pub/Sub — When you run multiple Bun instances behind a load balancer, use Redis to broadcast notifications across all server instances. This ensures a user connected to Server B receives notifications published by Server A.

  2. Connection Sharding by User ID — Distribute users across server instances using consistent hashing on userId. This minimizes cross-instance communication.

  3. PostgreSQL Connection Pooling with PgBouncer — At scale, direct PostgreSQL connections from every WebSocket handler will exhaust your database. PgBouncer in transaction mode handles this elegantly.

  4. Bun's Edge Runtime — Deploy your notification engine on Bun's edge runtime for global low-latency delivery. Indian users connecting from tier-2 and tier-3 cities will benefit significantly from edge deployment.

  5. Notification Queue with BullMQ — For high-volume scenarios (think: flash sales on Indian e-commerce platforms), queue notifications through BullMQ backed by Redis. This prevents overwhelming your WebSocket layer during traffic spikes.


Why This Stack Wins for Indian Developers

The Bun + Elysia + PostgreSQL combination is particularly well-suited for Indian full-stack developers in 2026:

  • Bun's speed means faster local development and CI/CD pipelines — critical when working with limited hardware
  • Elysia's minimal boilerplate lets you focus on business logic rather than framework configuration
  • PostgreSQL is free and open-source — no licensing costs for startups and freelancers
  • The entire stack runs on affordable cloud instances — a ₹2,000/month DigitalOcean droplet can handle thousands of concurrent WebSocket connections

Conclusion

You now have a complete real-time notification engine that leverages the best of Bun.js, Elysia.js, and PostgreSQL. The patterns covered here — WebSocket connection management, type-safe routing, PostgreSQL-backed persistence, and exponential backoff reconnection — form the foundation of any production real-time system.

As you extend this project, consider adding notification preferences per user, WebSocket compression for bandwidth-constrained environments, and end-to-end encryption for sensitive notification payloads.

The Indian tech ecosystem is building the next generation of real-time applications, and with tools like Bun and Elysia making the stack faster and simpler than ever, there's never been a better time to start.

Happy coding! 🚀

Enjoyed this article?

Read More Articles