TypeScript Realtime Package
On this page 10
A powerful and flexible realtime communication library for TypeScript that provides multiple drivers (Pusher, Socket.IO, and Bun WebSocket) for handling realtime events and broadcasting.
Installation
# Using bun
bun add @stacksjs/realtime
# Using npm
npm install @stacksjs/realtime
# Using yarn
yarn add @stacksjs/realtime
# Using pnpm
pnpm add @stacksjs/realtime
Basic Usage
import { broadcast, channel } from '@stacksjs/realtime'
// Broadcasting an event
const orderData = {
orderId: '12345',
userId: 'user_123',
products: [
{ id: 'prod_1', name: 'Widget', quantity: 2 },
{ id: 'prod_2', name: 'Gadget', quantity: 1 },
],
totalAmount: 99.99,
shippingAddress: {
street: '123 Main St',
city: 'Anytown',
country: 'USA',
postalCode: '12345',
},
}
// Trigger the broadcast
await broadcast('OrderShipped', orderData)
Creating Broadcast Events
Create a broadcast event by defining a handler in your broadcasts directory:
// app/Broadcasts/OrderShipped.ts
import type { RealtimeOptions } from '@stacksjs/types'
import { channel } from '@stacksjs/realtime'
interface OrderData {
orderId: string
userId: string
products: Array<{
id: string
name: string
quantity: number
}>
totalAmount: number
shippingAddress: {
street: string
city: string
country: string
postalCode: string
}
shippedAt?: string
}
export default {
/**
_ The event name.
_/
event: 'OrderShipped',
/**
_ Handle the broadcast event.
_ This method is called when the event is triggered.
*/
async handle(data: OrderData): Promise<void> {
await channel(`orders.${data.orderId}`).private(this.event, data)
},
} satisfies RealtimeOptions
Channel Types
The package supports three types of channels:
-
Public Channels
_ Accessible to all clients _ No authentication required
- Example:
channel('notifications').public(event, data)
- Example:
-
Private Channels
_ Require authentication _ Prefixed with
private-- Example:
channel('user-123').private(event, data)
- Example:
-
Presence Channels
_ Require authentication _ Support user presence features _ Prefixed with
presence-_ Example:channel('chat-room').presence(event, data)
Broadcasting Methods
Basic Broadcasting
// Broadcast to a public channel
await channel('notifications').public('new-message', { message: 'Hello!' })
// Broadcast to a private channel
await channel('user-123').private('status-update', { status: 'online' })
// Broadcast to a presence channel
await channel('chat-room').presence('user-joined', { user: 'John' })
Broadcasting with Events
// Define your event data interface
interface MessageData {
content: string
sender: string
timestamp: string
}
// Create a broadcast event
// app/Broadcasts/NewMessage.ts
export default {
event: 'NewMessage',
async handle(data: MessageData): Promise<void> {
await channel(`chat.${data.sender}`).private(this.event, data)
},
} satisfies RealtimeOptions
// Trigger the broadcast
await broadcast('NewMessage', {
content: 'Hello!',
sender: 'user_123',
timestamp: new Date().toISOString(),
})
Configuration
The package can be configured through the application's configuration file:
// config/realtime.ts
export default {
driver: 'pusher', // or 'socket' or 'bun'
// Pusher configuration
pusher: {
appId: 'your-app-id',
key: 'your-key',
secret: 'your-secret',
cluster: 'mt1',
useTLS: true,
},
// Socket.IO configuration
socket: {
host: 'localhost',
port: 3000,
},
}
Authenticated HTTP streams
Optional onEnqueue(chunk, broadcast) observes a successful local enqueue. The
chunk is the exact queued Uint8Array; broadcast metadata retains the original
channel, event and data identity. Connected and heartbeat frames pass
null metadata. Do not mutate these bytes. This is local queue admission, not a
client receipt. Optional onClose() runs once after subscription cleanup.
Both callbacks retain the subscriber's async context and options receiver.
They run synchronously without delaying delivery. A throwing or rejecting
enqueue observer closes the stream; close observer failures are contained.
Neither callback changes framing, authorization checks or queue limits.
createBroadcastHub() initializes the native broadcast engine without opening a
WebSocket listener. createBroadcastResponse({ channels, authorize, signal })
returns an SSE response for server-selected channels. Authorization runs before
subscription, each event and each heartbeat, in the subscriber's async context.
A broadcast from another request therefore checks the subscriber's identity.
The broadcaster's context is restored after the callback.
The callback must resolve current credentials, permissions and expiry each time. Context binding preserves request identity; it does not cache permission or provide a distributed revocation fence. Pass the request's abort signal for cleanup. Slow consumers are bounded and disconnected. Re-read durable state after reconnecting because this HTTP transport does not promise durable replay.
Best Practices
-
Channel Naming
_ Use descriptive names for channels _ Follow the naming convention:
{type}.{identifier}- Example:
orders.12345,chat.user_123
- Example:
-
Event Naming
_ Use PascalCase for event names _ Be specific and descriptive
- Example:
OrderShipped,MessageReceived
- Example:
-
Type Safety
_ Define interfaces for your event data _ Use TypeScript to ensure type safety
- Export types for reuse across your application
-
Security
_ Use private channels for sensitive data _ Implement proper authentication for private and presence channels
- Validate data before broadcasting
-
Organization
_ Keep broadcast events in a dedicated directory _ Group related events together
- Use consistent naming conventions