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

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, the controller emits a task.updated event immediately after persisting changes to the database:

// 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) 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 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. 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. This implementation publishes events to Redis Pub/Sub, allowing multiple API instances to share the same real-time stream:

// 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) 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) manages the user-scoped socket, while useProjectWebSocket (in apps/web/src/hooks/use-project-websocket.ts) handles project-specific connections:

// 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 (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), 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. 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) that functions without external dependencies for single-instance deployments. Redis is only required when scaling horizontally across multiple server instances.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →