| name | stacks-realtime |
| description | Use when implementing real-time features in Stacks — WebSocket broadcasting, public/private/presence channels, emit to users, the Channel class, broadcast discovery, server lifecycle, or realtime configuration. Covers @stacksjs/realtime and config/realtime.ts. |
| license | MIT |
| compatibility | Bun >= 1.3.0, TypeScript |
| allowed-tools | Read Edit Write Bash Grep Glob |
Stacks Realtime
WebSocket broadcasting via ts-broadcasting with channel-based messaging. Provides a Laravel-like API for real-time features.
Key Paths
- Core package:
storage/framework/core/realtime/src/
- Configuration:
config/realtime.ts
- Application broadcasts:
app/Broadcasts/
Source Files
realtime/src/
index.ts # re-exports ts-broadcasting + all Stacks-specific exports
emit.ts # emit(), emitToUser(), emitToUsers()
channel.ts # Channel class + channel() factory
broadcast.ts # Broadcast class + runBroadcast() + broadcast()
ws.ts # WebSocket request handler (legacy, kept for backward compat)
server-instance.ts # server lifecycle (createServer/getServer/setServer/stopServer)
Exports (from index.ts)
The package re-exports everything from ts-broadcasting plus Stacks-specific APIs:
export * from 'ts-broadcasting'
export { getServer, setServer, createServer, stopServer } from './server-instance'
export { emit, emitToUser, emitToUsers } from './emit'
export type { EmitOptions } from './emit'
export { channel as createChannel, Channel as StacksChannel } from './channel'
export { broadcast as dispatchBroadcast, runBroadcast, Broadcast as LegacyBroadcast } from './broadcast'
export type { BroadcastInstance } from './broadcast'
export { setBunSocket, handleWebSocketRequest, storeWebSocketEvent } from './ws'
Note the renamed exports:
channel() is exported as createChannel
Channel class is exported as StacksChannel
broadcast() is exported as dispatchBroadcast
Broadcast class is exported as LegacyBroadcast
Emit Functions
The primary API for broadcasting events.
emit(channel, event, data?, options?) - Broadcast to a channel
import { emit, emitToUser, emitToUsers } from '@stacksjs/realtime'
emit('chat-room', 'new-message', { text: 'Hello', sender: 'John' })
emit('orders', 'updated', { status: 'shipped' }, { private: true })
emit('room-1', 'user-joined', { userId: 42 }, { presence: true })
emit('chat', 'typing', data, { exclude: 'socket-id-1' })
emit('chat', 'typing', data, { exclude: ['socket-id-1', 'socket-id-2'] })
EmitOptions interface
interface EmitOptions {
private?: boolean
presence?: boolean
exclude?: string | string[]
driver?: string
}
emitToUser(userId, event, data?, options?) - Emit to specific user
Sends to private-user.{userId} channel. The target user must be subscribed to their own private user channel.
emitToUser(42, 'notification', { message: 'You have a new order' })
emitToUser('user-123', 'alert', { type: 'warning', text: 'Session expiring' })
emitToUsers(userIds, event, data?, options?) - Emit to multiple users
Iterates over userIds and calls emitToUser for each.
emitToUsers([42, 43, 44], 'announcement', { text: 'Server maintenance at 3pm' })
Channel Class
Provides a fluent API for broadcasting to channels with explicit channel types.
channel(name) - Factory function
import { createChannel } from '@stacksjs/realtime'
const ch = createChannel('orders')
await ch.public('new-order', { id: 1, total: 99.99 })
await ch.private('status-update', { status: 'shipped' })
await ch.presence('user-online', { userId: 42 })
await ch.broadcast('event', data, 'private')
await ch.broadcast('event', data)
Channel class internals
class Channel {
private channelName: string
constructor(channel: string)
async private(event: string, data?: any): Promise<void>
async public(event: string, data?: any): Promise<void>
async presence(event: string, data?: any): Promise<void>
async broadcast(event: string, data?: any, type: ChannelType = 'public'): Promise<void>
}
All methods throw Error('Broadcast server not initialized') if getServer() returns null.
Server Lifecycle
The server instance is stored as a module-level singleton (serverInstance).
import { createServer, getServer, setServer, stopServer } from '@stacksjs/realtime'
import type { ServerConfig, BroadcastServer } from 'ts-broadcasting'
const server: BroadcastServer = await createServer(config)
const server: BroadcastServer | null = getServer()
setServer(server)
await stopServer()
createServer() dynamically imports ts-broadcasting, instantiates BroadcastServer, calls start(), stores the instance via setServer(), and returns it.
Broadcast Discovery
Dynamically loads broadcast files from app/Broadcasts/ and executes them.
runBroadcast(name, payload?) - Run a broadcast file
import { runBroadcast, dispatchBroadcast } from '@stacksjs/realtime'
await runBroadcast('OrderStatusChanged', { orderId: 1 })
await dispatchBroadcast('NewMessage', { text: 'Hello' })
Broadcast file interface
interface BroadcastInstance {
channel?: () => string | string[]
broadcastOn?: () => string | string[]
event?: () => string
broadcastAs?: () => string
data?: () => any
broadcastWith?: () => any
handle?: (payload?: any) => Promise<void> | void
}
How broadcast discovery works
- Scans
app/Broadcasts/**/*.ts using bun.globSync()
- Finds file ending with
{name}.ts
- Imports the module and reads its
default export
- If
handle() exists, calls it with the payload and returns
- Otherwise, reads channel/event/data from the interface methods
- Constructs a
BroadcastEvent and calls server.broadcaster.broadcast(event)
Example broadcast file
export default {
broadcastOn: () => ['orders', 'private-admin'],
broadcastAs: () => 'order.created',
broadcastWith: () => ({ orderId: 123, total: 99.99 }),
}
Or with a custom handler:
export default {
handle: async (payload) => {
const { emit } = await import('@stacksjs/realtime')
emit(`private-user.${payload.userId}`, 'notification', {
message: payload.message,
}, { private: true })
},
}
Legacy/Backward Compatibility
Broadcast class (exported as LegacyBroadcast)
class Broadcast {
async connect(): Promise<void>
async disconnect(): Promise<void>
subscribe(channel, callback): void
unsubscribe(channel): void
broadcast(channel, event, data?, type?): void
isConnected(): boolean
}
WebSocket handler (ws.ts)
setBunSocket(server: BroadcastServer | null): void
storeWebSocketEvent(type, socket, details): Promise<void>
handleWebSocketRequest(req, server): Promise<Response | undefined>
config/realtime.ts
Full configuration with all options and their defaults:
export default {
enabled: true,
mode: 'server' as 'server' | 'serverless',
driver: 'bun' as 'socket' | 'pusher' | 'bun' | 'reverb' | 'ably',
server: {
host: env.BROADCAST_HOST || '0.0.0.0',
port: Number(env.BROADCAST_PORT || 6001),
scheme: 'ws' as 'ws' | 'wss',
driver: 'bun',
redis: {
enabled: Boolean(env.BROADCAST_REDIS_ENABLED || false),
host: env.REDIS_HOST || 'localhost',
port: Number(env.REDIS_PORT || 6379),
password: env.REDIS_PASSWORD || ,
: env. || ,
},
: {
: ,
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
},
: {
: ,
: ,
: ,
},
: {
: (env. || ),
: ,
: ,
},
},
: {
: ,
: ,
: env. || ,
: ,
: ,
},
: {
: ,
: ,
: {
: ,
: ,
: ,
},
},
: {
: env. || ,
: env. || ,
: env. || ,
},
: {
: (env. || ),
: env. || ,
: {
: env. || env. || ,
: [, ],
},
},
: {
: env. || ,
: env. || ,
: env. || ,
: env. || ,
: (env. ?? ),
},
: (env. || ),
}
Environment Variables
| Variable | Default | Description |
|---|
REALTIME_MODE | server | Deployment mode: server or serverless |
BROADCAST_DRIVER | bun | Broadcasting driver |
BROADCAST_HOST | 0.0.0.0 | Server bind host |
BROADCAST_PORT | 6001 | Server port |
BROADCAST_SCHEME | ws | WebSocket scheme (ws/wss) |
BROADCAST_REDIS_ENABLED | false | Enable Redis adapter |
REDIS_HOST | localhost | Redis host |
REDIS_PORT | 6379 | Redis port |
REDIS_PASSWORD | (empty) | Redis password |
BROADCAST_REDIS_PREFIX | stacks:realtime: | Redis key prefix |
BROADCAST_RATE_LIMIT_ENABLED | true | Enable rate limiting |
BROADCAST_METRICS_ENABLED | false | Enable metrics endpoint |
BROADCAST_APP_ID | stacks | App ID |
BROADCAST_APP_KEY | (empty) | App key |
BROADCAST_APP_SECRET | (empty) | App secret |
BROADCAST_CORS_ORIGIN | APP_URL | CORS origin |
BROADCAST_DEBUG | false | Debug mode |
PUSHER_APP_ID | (empty) | Pusher app ID |
Gotchas
- The server must be created via
createServer() before emit() works -- otherwise it silently logs a warning and returns
- Channel and Broadcast methods throw errors if the server is not initialized (unlike
emit() which only warns)
- Private channels auto-prefix
private- -- don't add it yourself (the code checks channel.startsWith('private-'))
- Presence channels auto-prefix
presence- -- same logic applies
emitToUser() sends to private-user.{userId} -- users must subscribe to this channel pattern
- When using
emit() with exclude, only the first socket ID in an array is used (BroadcastServer limitation)
- Broadcast files are dynamically imported from
app/Broadcasts/ using bun.globSync
- The
Broadcast class is legacy -- new code should use emit() and channel() directly
handleWebSocketRequest() and storeWebSocketEvent() are deprecated -- ws events are tracked internally by ts-broadcasting
setBunSocket() is deprecated -- use setServer() instead
- Serverless mode uses API Gateway WebSocket for AWS Lambda with DynamoDB for connection management
- Rate limiting defaults: 100 connections per IP, 50 messages/second, 64KB max payload, 300s ban duration
- Redis adapter enables horizontal scaling across multiple server instances
- Auto-scaling config (min/max/targetCPU) is for cloud deployment orchestration
- The
satisfies RealtimeConfig type annotation ensures the config matches the expected type from @stacksjs/types