Skip to content

Broadcasting / Real-time

CruzJS provides real-time communication between server and clients through Server-Sent Events (SSE), with built-in presence tracking for showing who is online.

Register the BroadcastModule in your application:

import { BroadcastModule } from '@cruzjs/core/broadcasting';
export default createCruzApp({
modules: [BroadcastModule],
});

The BroadcastService is the server-side API for publishing messages and managing presence.

import { Injectable, Inject } from '@cruzjs/core/di';
import { BroadcastService } from '@cruzjs/core/broadcasting';
@Injectable()
export class OrderService {
constructor(
@Inject(BroadcastService) private readonly broadcast: BroadcastService,
) {}
async placeOrder(orgId: string, order: Order) {
// ... save order ...
await this.broadcast.publish(`org:${orgId}:orders`, 'order.created', {
orderId: order.id,
total: order.total,
});
}
}
MethodDescription
publish(channel, event, data)Send a message to all subscribers on a channel
getPresence(channel)Get a list of members currently in a presence channel
joinPresence(channel, member)Add a member to a presence channel
leavePresence(channel, member)Remove a member from a presence channel

The module registers an SSE endpoint at /api/broadcast/sse. Clients connect with a channel query parameter:

GET /api/broadcast/sse?channel=org:acme:orders

The response uses Content-Type: text/event-stream and streams BroadcastMessage objects:

type BroadcastMessage = {
event: string;
data: unknown;
timestamp: string;
};

Use useBroadcast in React components to subscribe to a channel:

import { useBroadcast } from '@cruzjs/core/broadcasting/client';
function OrderFeed() {
const { messages, presenceMembers } = useBroadcast('org:acme:orders');
return (
<div>
<p>{presenceMembers.length} users online</p>
<ul>
{messages.map((msg, i) => (
<li key={i}>
{msg.event}: {JSON.stringify(msg.data)}
</li>
))}
</ul>
</div>
);
}

The hook manages the SSE connection lifecycle automatically — it connects on mount and disconnects on unmount.

Any connected client can subscribe. No authentication required.

useBroadcast('announcements');

Require authentication. Prefix the channel name with private-:

useBroadcast('private-user:abc123');

The SSE endpoint validates the session token before allowing the connection.

Track which members are currently subscribed. Prefix with presence-:

const { presenceMembers } = useBroadcast('presence-workspace:design');
// presenceMembers: [{ id: 'user1', name: 'Alice' }, { id: 'user2', name: 'Bob' }]

Members are automatically added when they connect and removed when they disconnect.

The default backend uses KVSSEBackend. Messages are published to KV with short TTLs, and presence data is stored in KV with a 24-hour TTL. Clients poll KV for new messages on the SSE connection.

For container-based deployments, the broadcast system uses Redis pub/sub for cross-instance message delivery. Configure via the DockerAdapter:

import { DockerAdapter } from '@cruzjs/adapter-docker';
export default createCruzApp({
adapter: new DockerAdapter(),
modules: [BroadcastModule],
});

Set the REDIS_URL environment variable to connect to your Redis instance.

Server-side — publish when a notification is created:

await this.broadcast.publish(
`private-user:${userId}`,
'notification.new',
{ count: unreadCount },
);

Client-side — update the badge in real time:

function NotificationBadge({ userId }: { userId: string }) {
const { messages } = useBroadcast(`private-user:${userId}`);
const [count, setCount] = useState(0);
useEffect(() => {
const latest = messages.findLast((m) => m.event === 'notification.new');
if (latest) {
setCount((latest.data as { count: number }).count);
}
}, [messages]);
if (count === 0) return null;
return <span className="badge">{count}</span>;
}
function WorkspacePresence({ workspaceId }: { workspaceId: string }) {
const { presenceMembers } = useBroadcast(`presence-workspace:${workspaceId}`);
return (
<div className="flex gap-1">
{presenceMembers.map((member) => (
<Avatar key={member.id} name={member.name} size="sm" />
))}
</div>
);
}