How Kaneo Handles Real-Time Event Broadcasting: Architecture and Implementation
Kaneo implements real-time event broadcasting through a lightweight event-bus that connects API controllers to WebSocket clients via pluggable broadcast adapters, supporting both single-instance deployments with an in-memory adapter and multi-instance scaling with Redis Pub/Sub.
Kaneo is an open-source project management platform that requires instant UI updates when tasks change or notifications arrive. Its real-time event broadcasting system separates event production from transport, allowing developers to scale from a single Node.js process to a distributed cluster without changing application code. This architecture ensures reliable message delivery whether running locally or across multiple API instances.
The Event-Bus Architecture
Kaneo's real-time system is built around a centralized event-bus that normalizes payloads and forwards them to a broadcast adapter. This design cleanly separates event production, transport, and consumption, making the system extensible and easy to test.
When an API controller modifies state—such as creating a task or updating a project—it calls publishEvent(eventName, payload). This function standardizes the message format and routes it to the configured adapter, which then handles distribution to connected WebSocket clients.
Broadcast Adapter Interface
All transport mechanisms implement the BroadcastAdapter interface defined in apps/api/src/ws/broadcast-adapter.ts. This contract ensures consistent behavior whether broadcasting in-memory or across a Redis cluster.
export type BroadcastMessage = {
projectId: string;
message: { type: string; projectId: string; taskId?: string };
excludeInitiatorId?: string;
};
export type UserBroadcast = {
userId: string;
message: { type: string; [key: string]: unknown };
origin?: string;
};
export type BroadcastAdapter = {
publish(msg: BroadcastMessage): Promise<void>;
publishToUser(msg: UserBroadcast): Promise<void>;
subscribe(handler: (msg: BroadcastMessage) => void): Promise<void>;
subscribeToUser(handler: (msg: UserBroadcast) => void): Promise<void>;
shutdown(): Promise<void>;
};
InMemoryBroadcastAdapter for Single-Instance Deployments
For single-instance deployments, Kaneo uses InMemoryBroadcastAdapter located in apps/api/src/ws/in-memory-broadcast-adapter.ts. This adapter delivers messages directly inside the same Node process by storing handler references in memory, avoiding any external dependencies.
export class InMemoryBroadcastAdapter implements BroadcastAdapter {
private handler?: (msg: BroadcastMessage) => void;
private userHandler?: (msg: UserBroadcast) => void;
async publish(msg) { this.handler?.(msg); }
async publishToUser(msg) { this.userHandler?.(msg); }
async subscribe(handler) { this.handler = handler; }
async subscribeToUser(handler) { this.userHandler = handler; }
async shutdown() { this.handler = undefined; this.userHandler = undefined; }
}
This implementation is ideal for development environments or small deployments where horizontal scaling is not required.
RedisBroadcastAdapter for Multi-Instance Scaling
When running multiple API instances, Kaneo switches to RedisBroadcastAdapter in apps/api/src/ws/redis-broadcast-adapter.ts. This adapter publishes messages to Redis Pub/Sub channels using a pattern like kaneo:ws:*:broadcast, ensuring every node receives broadcasts regardless of which instance originated the event.
await getRedisPub().publish(this.channelForProject(msg.projectId), JSON.stringify(msg));
await getRedisSub().psubscribe(CHANNEL_PATTERN);
(getRedisSub() as Redis).on("pmessage", this._pmessageHandler);
The Redis adapter exposes the same interface as the in-memory version, allowing zero-downtime transport swapping via environment configuration.
Publishing Events from Controllers
Controllers throughout the API layer invoke publishEvent() to emit real-time updates. For example, when creating a task, the controller persists the data to the database and immediately broadcasts the change to all connected project members.
import { publishEvent } from "../../events";
export async function createTask(req) {
// … create task in DB …
await publishEvent("task.created", { taskId, projectId });
}
The publishEvent implementation (located in apps/api/src/events/) normalizes the payload and forwards it to the active broadcast adapter, decoupling business logic from transport mechanics.
Client-Side WebSocket Consumption
When a browser connects, the API creates a WebSocket connection that registers a handler with the chosen adapter using subscribe or subscribeToUser. The handler receives BroadcastMessage or UserBroadcast objects and forwards them over the socket.
In the React front-end, hooks such as useProjectWebsocket (defined in apps/web/src/hooks/use-project-websocket.ts) manage subscription lifecycles. These hooks listen for project-level or user-level messages and update TanStack Query caches, causing UI components to re-render instantly.
export function useProjectWebsocket(projectId: string) {
const ws = useWebsocket(); // low‑level socket wrapper
useEffect(() => {
const handler = (msg) => ws.emit(msg);
ws.broadcastAdapter.subscribe(handler);
return () => ws.broadcastAdapter.shutdown();
}, [projectId, ws]);
}
Scalability Considerations
Kaneo's broadcast architecture supports two distinct deployment modes without code changes:
- Single-instance deployments rely on
InMemoryBroadcastAdapter, eliminating external dependencies and minimizing latency for local development or small teams. - Multi-instance deployments enable
RedisBroadcastAdapter, which subscribes all API nodes to thekaneo:ws:*:broadcastpattern. This ensures that a task update originating on Server A reaches a WebSocket client connected to Server B within milliseconds.
The adapter pattern allows operators to scale from a single process to a distributed cluster simply by setting environment variables, with no changes to controller or client code.
Summary
- Event-bus architecture decouples event production from delivery, allowing controllers to broadcast without knowing transport details.
- BroadcastAdapter interface standardizes implementations, enabling seamless swapping between in-memory and Redis transports.
- InMemoryBroadcastAdapter handles single-instance deployments with zero external dependencies.
- RedisBroadcastAdapter uses Pub/Sub patterns to synchronize events across multiple API instances.
- React hooks consume WebSocket messages and synchronize TanStack Query caches for instant UI updates.
Frequently Asked Questions
What interface must broadcast adapters implement in Kaneo?
All broadcast adapters must implement the BroadcastAdapter type defined in apps/api/src/ws/broadcast-adapter.ts. This interface requires five methods: publish, publishToUser, subscribe, subscribeToUser, and shutdown, ensuring consistent behavior across in-memory and Redis implementations.
How does Kaneo scale real-time event broadcasting across multiple servers?
Kaneo scales horizontally using RedisBroadcastAdapter, which publishes messages to Redis Pub/Sub channels matching the pattern kaneo:ws:*:broadcast. Every API instance subscribes to these channels, ensuring that events created on one server reach WebSocket clients connected to any other instance in the cluster.
How do client-side React hooks consume real-time events?
The front-end uses specialized hooks like useProjectWebsocket that call the adapter's subscribe method when mounting and shutdown when unmounting. These hooks receive BroadcastMessage objects and update TanStack Query caches, which automatically triggers re-renders in React components displaying the project data.
Can Kaneo handle real-time updates without Redis?
Yes, single-instance deployments use InMemoryBroadcastAdapter, which stores message handlers directly in the Node.js process memory. This approach requires no external dependencies and works immediately for local development or single-server production environments.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →