# How Kaneo Implements Real-Time Updates: Event-Driven WebSocket Architecture

> Discover how Kaneo implements real-time updates with its event-driven WebSocket architecture. Learn about pluggable broadcast adapters for in-memory and Redis messaging.

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

---

**Kaneo pushes real-time updates to connected browsers using an event-driven WebSocket system with pluggable broadcast adapters that support both in-memory and Redis-backed messaging.**

Kaneo is an open-source project management platform that keeps board states synchronized across clients without requiring page refreshes. Understanding how Kaneo implements real-time updates reveals a lightweight yet scalable architecture that combines domain events, dual-purpose WebSocket endpoints, and React hooks to propagate changes instantly from server to UI.

## Event Publishing from API Controllers

Every state-changing operation in Kaneo’s API publishes a domain event through a centralized `publishEvent` utility. When a controller successfully mutates data, it immediately notifies the broadcast layer so WebSocket clients can react.

In [`apps/api/src/task/controllers/update-task.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/task/controllers/update-task.ts), the controller emits a **task.updated** event immediately after persisting changes to the database:

```typescript
// apps/api/src/task/controllers/update-task.ts
import { publishEvent } from "../../events";

export async function updateTask(c) {
  // …perform DB update…
  await publishEvent("task.updated", { taskId, changes });
}

```

The `publishEvent` function (located in [`apps/api/src/events/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/events/index.ts)) decouples the business logic from transport concerns, allowing controllers to remain agnostic about whether updates are delivered via in-memory channels or Redis Pub/Sub.

## WebSocket Broadcast Adapters

The WebSocket layer in [`apps/api/src/ws/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/index.ts) selects a **broadcast adapter** based on environment configuration. This adapter pattern enables Kaneo to run on a single instance during development while scaling horizontally in production.

### In-Memory Adapter

For single-instance deployments where Redis is unavailable, Kaneo uses the in-memory broadcast adapter defined 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 maintains a local registry of WebSocket connections and broadcasts events directly to eligible sockets without external dependencies.

### Redis Broadcast Adapter

When configured with Redis, Kaneo switches to the **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 implementation publishes events to Redis Pub/Sub, allowing multiple API instances to share the same real-time stream:

```typescript
// apps/api/src/ws/redis-broadcast-adapter.ts
export class RedisBroadcastAdapter implements BroadcastAdapter {
  constructor(private client: Redis) {}
  async broadcast(event: string, payload: any) {
    await this.client.publish(event, JSON.stringify(payload));
  }
}

```

According to the Kaneo source code, the adapter selection logic checks environment variables to determine whether Redis is configured, ensuring seamless failover between modes.

## Connection Scoping and Endpoints

Kaneo maintains two distinct WebSocket endpoints to separate user-specific notifications from project-wide board updates.

### User-Scoped Connections

The `/ws/user` endpoint (defined in [`apps/api/src/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/index.ts)) tracks connections keyed by `userId`. This channel delivers personal notifications—such as `NOTIFICATION_CREATED` events—only to the specific user who should receive them.

### Project-Scoped Connections

The `/ws/:projectId` endpoint groups connections by `projectId`, enabling collaborative features where multiple users viewing the same Kanban board receive simultaneous updates. When a task moves or a new issue is created, the server pushes the event to every connection subscribed to that project identifier.

## Client-Side WebSocket Hooks

The React frontend maintains persistent WebSocket connections through custom hooks that handle authentication, reconnection, and cache invalidation.

**`useUserWebSocket`** (located in [`apps/web/src/hooks/use-user-websocket.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/hooks/use-user-websocket.ts)) manages the user-scoped socket, while **`useProjectWebSocket`** (in [`apps/web/src/hooks/use-project-websocket.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/hooks/use-project-websocket.ts)) handles project-specific connections:

```typescript
// apps/web/src/hooks/use-project-websocket.ts
export function useProjectWebSocket(projectId: string) {
  const wsRef = useRef<WebSocket | null>(null);
  useEffect(() => {
    const ws = new WebSocket(`${API_URL}/ws/${projectId}`);
    ws.onmessage = (e) => {
      const { event, data } = JSON.parse(e.data);
      // Invalidate TanStack Query caches as needed
      queryClient.invalidateQueries([event, projectId]);
    };
    wsRef.current = ws;
    return () => ws.close();
  }, [projectId]);
}

```

These hooks parse incoming messages and invalidate TanStack Query caches, triggering React components to re-render with fresh data without manual refetching.

## The Real-Time Data Flow

The complete propagation flow follows these steps:

1. **Mutation** – A client POSTs an update to [`update-task.ts`](https://github.com/usekaneo/kaneo/blob/main/update-task.ts) (or similar controller).
2. **Event Emission** – The controller calls `publishEvent("task.updated", payload)`.
3. **Adapter Broadcast** – The event routes through either the in-memory or Redis broadcast adapter.
4. **Socket Distribution** – The adapter emits the message over WebSocket connections filtered by scope (user or project).
5. **Client Update** – The browser receives the message via `useProjectWebSocket`, invalidates the query cache, and the UI re-renders instantly.

## Scaling with Redis Pub/Sub

When deployed with Redis (as documented in [`ENVIRONMENT_SETUP.md`](https://github.com/usekaneo/kaneo/blob/main/ENVIRONMENT_SETUP.md)), Kaneo’s real-time system supports horizontal scaling across multiple API instances. The Redis broadcast adapter ensures that a task updated on Server A reaches users connected to Server B by fanning out messages through the shared Pub/Sub layer. This architecture preserves real-time consistency without requiring sticky sessions or complex coordination between nodes.

## Summary

- **Event-driven architecture**: Controllers publish domain events via `publishEvent` to decouple business logic from transport mechanisms.
- **Adapter pattern**: The system switches between in-memory and Redis broadcast adapters based on environment configuration.
- **Dual endpoints**: Separate `/ws/user` and `/ws/:projectId` endpoints handle notifications and collaborative board updates respectively.
- **React integration**: Custom hooks `useUserWebSocket` and `useProjectWebSocket` maintain connections and synchronize TanStack Query caches.
- **Horizontal scaling**: Redis Pub/Sub enables multi-instance deployments while maintaining real-time consistency across all connected clients.

## Frequently Asked Questions

### What transports does Kaneo use for real-time updates?

Kaneo uses **WebSockets** as the primary transport protocol. The API exposes native WebSocket endpoints that the React frontend connects to using the browser's standard `WebSocket` object, providing full-duplex communication without polling overhead.

### How does Kaneo handle real-time updates in multi-instance deployments?

When running multiple API instances, Kaneo configures the **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 events to Redis Pub/Sub, ensuring that messages broadcast from one instance reach WebSocket clients connected to any other instance in the cluster.

### What client-side technology receives WebSocket events in Kaneo?

The frontend uses **custom React hooks**—specifically `useUserWebSocket` and `useProjectWebSocket`—to maintain persistent WebSocket connections. These hooks parse incoming JSON messages and invalidate TanStack Query caches, causing components to re-render with updated data.

### Does Kaneo require Redis for real-time functionality?

No. Kaneo includes an **in-memory broadcast adapter** ([`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)) that functions without external dependencies for single-instance deployments. Redis is only required when scaling horizontally across multiple server instances.