# How Kaneo Handles Real-Time Event Broadcasting: Architecture and Implementation

> Explore Kaneo's real-time event broadcasting architecture. Discover how its lightweight event-bus and pluggable adapters enable efficient WebSocket communication for single or multi-instance deployments.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: architecture
- Published: 2026-08-29

---

**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`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/broadcast-adapter.ts). This contract ensures consistent behavior whether broadcasting in-memory or across a Redis cluster.

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/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.

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/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.

```typescript
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.

```typescript
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`](https://github.com/usekaneo/kaneo/blob/main/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.

```typescript
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 the `kaneo:ws:*:broadcast` pattern. 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`](https://github.com/usekaneo/kaneo/blob/main/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.