GitHub - stacksjs/ts-broadcasting: Realtime the simple & performant way.

Social Card of this repo

A high-performance, real-time WebSocket broadcasting system for TypeScript, built on Bun. Inspired by Laravel's broadcasting system, it provides a type-safe, feature-rich solution for real-time communication with support for public, private, and presence channels.

Features

  • Built on Bun - Leverages Bun's native WebSocket implementation for maximum performance
  • Channel Authorization - Flexible authorization for private and presence channels with pattern matching
  • Presence Channels - Track who's online in real-time with custom member data
  • Laravel Echo Compatible - Drop-in replacement client API for Laravel Echo
  • Type-Safe - Full TypeScript support with generics throughout
  • Vue & Svelte - First-class framework integrations with composables and stores
  • Redis Scaling - Horizontal scaling via Redis pub/sub adapter
  • End-to-End Encryption - AES-256-GCM encryption for sensitive channels
  • Queued Broadcasting - Background job queuing with delayed and recurring broadcasts
  • Webhooks - HTTP webhook notifications on channel events
  • Prometheus Metrics - Built-in /metrics endpoint for observability
  • Message Persistence - Store and replay message history
  • Rate Limiting & Auth - Built-in middleware for security
  • CLI - Command-line interface for server management
  • Circuit Breaker - Failure handling for external service resilience
  • Load Management - Backpressure handling, connection limits, and load shedding

Install

Note

ts-broadcasting requires Bun as its runtime.

Quick Start

Server

import { BroadcastServer } from 'ts-broadcasting'

const server = new BroadcastServer({
  driver: 'bun',
  connections: {
    bun: {
      driver: 'bun',
      host: '0.0.0.0',
      port: 6001,
    },
  },
})

// Authorize private channels
server.channels.channel('private-user.{userId}', (ws, params) => {
  return ws.data.user?.id === params?.userId
})

// Authorize presence channels
server.channels.channel('presence-chat.{roomId}', (ws) => {
  return {
    id: ws.data.user?.id,
    info: { name: ws.data.user?.name },
  }
})

await server.start()

Broadcasting Events

// Direct broadcasting
server.broadcast('notifications', 'NewNotification', {
  title: 'Hello',
  body: 'World',
})

// Using the Broadcaster
server.broadcaster.send('orders', 'OrderCreated', { id: 1, total: 99.99 })

// Exclude sender
server.broadcaster.toOthers(socketId).send('chat', 'NewMessage', { text: 'Hi' })

// Using event objects
import { createEvent } from 'ts-broadcasting'

const event = createEvent('private-orders.123', 'OrderShipped', {
  orderId: 123,
  trackingNumber: 'ABC123',
})

await server.broadcaster.broadcast(event)

// Fluent anonymous events
import { AnonymousEvent } from 'ts-broadcasting'

new AnonymousEvent('notifications')
  .as('SystemAlert')
  .with({ type: 'maintenance', message: 'Restart in 5 minutes' })
  .send(server.broadcaster)

Facade API (Laravel-style)

import { Broadcast, broadcast, broadcastToUser, channel } from 'ts-broadcasting'

Broadcast.setServer(server)

// Define channel authorization
channel('private-orders.{orderId}', (socket, params) => {
  return socket.data.user?.id === getOrderOwnerId(params?.orderId)
})

// Broadcast events
Broadcast.send('orders', 'OrderCreated', { id: 1 })
Broadcast.private('user.123', 'NotificationReceived', { message: 'Hello!' })
Broadcast.presence('chat.room1', 'NewMessage', { text: 'Hi!' })
Broadcast.toUser(123, 'Notification', { title: 'Welcome' })
Broadcast.toUsers([1, 2, 3], 'Announcement', { message: 'Hello everyone!' })

// Helper functions
await broadcast(new OrderShipped(order))
broadcast('orders', 'OrderCreated', { id: 1 })
broadcastToUser(123, 'Notification', { title: 'Welcome' })

Client (Browser)

import { BroadcastClient } from 'ts-broadcasting'

const client = new BroadcastClient({
  broadcaster: 'bun',
  host: 'localhost',
  port: 6001,
  scheme: 'ws',
  autoConnect: true,
})

// Public channel
client.channel('announcements')
  .listen('NewAnnouncement', (data) => {
    console.log('Announcement:', data)
  })

// Private channel
client.private('orders.123')
  .listen('OrderShipped', (data) => {
    console.log('Order shipped:', data)
  })

// Presence channel
const presence = client.join('chat.room1')
presence.here((members) => console.log('Online:', members))
presence.joining((member) => console.log('Joined:', member))
presence.leaving((member) => console.log('Left:', member))
presence.listen('NewMessage', (data) => console.log('Message:', data))

// Client events (whisper)
client.private('chat.room1').whisper('typing', { typing: true })

Vue 3 Integration

import { useBroadcast, useChannel, usePresence } from 'ts-broadcasting/vue'

const { client, isConnected } = useBroadcast({
  broadcaster: 'bun',
  host: 'localhost',
  port: 6001,
})

const { isSubscribed } = useChannel(client, 'announcements', {
  NewAnnouncement: (data) => console.log(data),
})

const { members, memberCount } = usePresence(client, 'chat.room1', {
  NewMessage: (data) => console.log(data),
})

Configuration

ts-broadcasting loads configuration via bunfig from a broadcast.config.ts (or realtime.config.ts) file:

// broadcast.config.ts
import type { BroadcastConfig } from 'ts-broadcasting'

const config: BroadcastConfig = {
  verbose: false,
  driver: 'bun',
  default: 'bun',

  connections: {
    bun: {
      driver: 'bun',
      host: '0.0.0.0',
      port: 6001,
      scheme: 'ws',
      options: {
        idleTimeout: 120,
        maxPayloadLength: 16 * 1024 * 1024,
        backpressureLimit: 1024 * 1024,
        sendPings: true,
        publishToSelf: false,
        perMessageDeflate: true,
      },
    },

    reverb: {
      driver: 'reverb',
      host: '127.0.0.1',
      port: 8080,
      scheme: 'ws',
      key: process.env.REVERB_APP_KEY,
      secret: process.env.REVERB_APP_SECRET,
      appId: process.env.REVERB_APP_ID,
    },

    pusher: {
      driver: 'pusher',
      key: process.env.PUSHER_APP_KEY,
      secret: process.env.PUSHER_APP_SECRET,
      appId: process.env.PUSHER_APP_ID,
      cluster: process.env.PUSHER_APP_CLUSTER || 'mt1',
      useTLS: true,
    },
  },
}

export default config

Server Configuration

The ServerConfig extends BroadcastConfig with additional options for advanced features:

import type { ServerConfig } from 'ts-broadcasting'

const serverConfig: ServerConfig = {
  // Base config
  driver: 'bun',
  verbose: true,

  // Redis for horizontal scaling
  redis: {
    host: 'localhost',
    port: 6379,
  },

  // Authentication
  auth: {
    enabled: true,
    jwt: { secret: 'your-jwt-secret', algorithm: 'HS256' },
  },

  // Rate limiting
  rateLimit: {
    max: 100,
    window: 60000,
  },

  // End-to-end encryption
  encryption: {
    enabled: true,
    algorithm: 'aes-256-gcm',
  },

  // Webhooks
  webhooks: {
    enabled: true,
    endpoints: [{
      url: 'https://api.example.com/webhooks',
      events: ['connection', 'subscribe', 'broadcast'],
    }],
  },

  // Message persistence
  persistence: {
    enabled: true,
    ttl: 3600000,
    maxMessages: 1000,
  },

  // Presence heartbeat
  heartbeat: {
    enabled: true,
    interval: 30000,
    timeout: 60000,
  },

  // Load management
  loadManagement: {
    maxConnections: 10000,
    maxChannelsPerConnection: 100,
    shedLoadAt: 90,
  },

  // Queue system
  queue: {
    enabled: true,
    defaultQueue: 'broadcasts',
    retry: { attempts: 3, backoff: { type: 'exponential', delay: 1000 } },
  },
}

CLI

# Start the server
broadcast start
broadcast start --host 0.0.0.0 --port 6001 --verbose

# Show server statistics
broadcast stats
broadcast stats --watch --interval 2

# Show current configuration
broadcast config

# Show version
broadcast version

HTTP Endpoints

The server exposes these HTTP endpoints:

Endpoint Description
/health Health check (returns { status: 'ok', redis: ... })
/stats Server statistics (connections, channels, uptime, metrics)
/metrics Prometheus-format metrics for monitoring
/app, /ws WebSocket upgrade endpoints

Channel Types

Type Prefix Auth Required Tracks Members
Public (none) No No
Private private- Yes No
Presence presence- Yes Yes

Documentation

For full documentation, visit ts-broadcasting.netlify.app.

Testing

Changelog

Please see our releases page for more information on what has changed recently.

Contributing

Please review the Contributing Guide for details.

Community

For help, discussion about best practices, or any other conversation that would benefit from being searchable:

Discussions on GitHub

For casual chit-chat with others using this package:

Join the Stacks Discord Server

Postcardware

Stacks OSS will always stay open-source, and we do love to receive postcards from wherever Stacks is used! We also publish them on our website. And thank you, Spatie.

Our address: Stacks.js, 12665 Village Ln #2306, Playa Vista, CA 90094

Sponsors

We would like to extend our thanks to the following sponsors for funding Stacks development. If you are interested in becoming a sponsor, please reach out to us.

Credits

License

The MIT License (MIT). Please see LICENSE for more information.

Made with 💙