# How Kaneo Uses WebSockets for Real-Time Task Updates and Notifications

> Discover how Kaneo leverages WebSockets for instant task updates and notifications. Learn about its pluggable BroadcastAdapter for scalable real-time features.

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

---

**Kaneo delivers real-time updates through a lightweight WebSocket layer that tracks project-scoped and user-scoped connections, using a pluggable BroadcastAdapter to support both single-instance deployments and Redis-backed horizontal scaling.**

Kaneo is an open-source project management platform that synchronizes team activity through instant UI updates. The application implements a custom WebSocket architecture within its API service to push live task changes, status updates, and notifications without requiring page refreshes. This system is designed to be transport-agnostic, allowing it to scale from single-instance deployments to multi-node clusters using Redis pub/sub.

## Connection Management and Scoping

The WebSocket implementation maintains two distinct connection registries to separate project-wide broadcasts from private user notifications. This scoping happens in [`apps/api/src/ws/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/index.ts), where the system initializes Maps to track active sockets.

### Tracking Project and User Connections

When a client connects, the API registers the socket in either `projectConnections` or `userConnections` depending on the subscription type. The `addConnection()` function in [`apps/api/src/ws/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/index.ts) handles this registration, associating each WebSocket with a specific project ID, user ID, and a unique initiator ID for exclusion logic【^1†L17-L34】.

- **Project-scoped connections** live in the `projectConnections` Map and receive task updates, label changes, and board modifications.
- **User-scoped connections** live in `userConnections` and receive private notifications such as `NOTIFICATION_CREATED` events.

When a client disconnects, `removeConnection()` cleans up the registry to prevent memory leaks【^1†L39-L50】.

```typescript
// Register a connection when a client subscribes to a project
import { addConnection } from "@kaneo/api/ws";
const conn = addConnection(projectId, wsContext, userId, initiatorId);

// Clean up on disconnect
import { removeConnection } from "@kaneo/api/ws";
removeConnection(projectId, conn);

```

## The Broadcast Abstraction Layer

Kaneo abstracts message delivery behind a `BroadcastAdapter` interface, allowing the same business logic to operate in development (single instance) and production (multi-instance) environments without code changes.

### The BroadcastAdapter Interface

The interface defined in [`apps/api/src/ws/broadcast-adapter.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/broadcast-adapter.ts) specifies two channels: `PROJECT_BROADCAST` and `USER_BROADCAST`. Any adapter must implement the `publish()` method to distribute messages across these channels【^2†L26-L39】.

### Adapter Implementations

The API ships with two concrete implementations:

- **InMemoryBroadcastAdapter** ([`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)): Delivers messages directly to local handlers within the same process. This is ideal for development and single-instance deployments【^3†L7-L22】.
- **RedisBroadcastAdapter** ([`apps/api/src/ws/redis-broadcast-adapter.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/redis-broadcast-adapter.ts)): Publishes messages to Redis pub/sub channels, enabling stateless API instances to share broadcast streams.

On startup, `initializeWebSocketAdapter()` in [`apps/api/src/ws/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/index.ts) selects the appropriate adapter based on whether Redis is configured (`isRedisConfigured()`), then subscribes to both broadcast streams【^1†L6-L14】【^1†L15-L33】.

## Broadcasting Strategies and Message Delivery

The WebSocket layer optimizes message delivery through intelligent batching for project updates while maintaining immediacy for user-specific alerts.

### Batched Project Broadcasts

The `broadcastToProject()` function manages a per-project message queue. When multiple updates occur rapidly—such as during bulk task operations—the system aggregates these into a single batch. A 100ms timeout ensures quick delivery while reducing network overhead【^1†L52-L64】.

Messages include an optional `excludeInitiatorId` parameter to prevent echoing updates back to the client that triggered the change:

```typescript
import { broadcastToProject } from "@kaneo/api/ws";

// Broadcast task update to all project members except the editor
broadcastToProject(projectId, {
  type: "TASK_UPDATED",
  projectId,
  taskId,
  changes: { status: "completed" }
}, initiatorId);

```

### Direct User Notifications

For time-sensitive alerts, `broadcastToUser()` bypasses batching and delivers immediately. The function first attempts local delivery via `deliverToLocalUserConnections()`, then forwards to the adapter for remote instances. Remote instances ignore messages tagged with their own instance UUID to prevent duplicate delivery【^1†L66-L84】.

```typescript
import { broadcastToUser } from "@kaneo/api/ws";

// Push a private notification to a specific user
broadcastToUser(userId, { 
  type: "NOTIFICATION_CREATED",
  notificationId: "123e4567-e89b-12d3-a456-426614174000"
});

```

## Event-Driven Real-Time Updates

Kaneo decouples business logic from transport details through an internal event system. The WebSocket layer subscribes to domain events and translates them into broadcast calls.

When the `notification.created` event fires, the handler invokes `broadcastToUser()` to push the alert to the recipient's private connection【^1†L37-L44】. Similarly, project-wide events like `task.created`, `task.moved`, or `label.updated` trigger `broadcastToProject()`, ensuring all active project participants see changes instantaneously without polling.

## Scaling with Redis Pub/Sub

For horizontally scaled deployments, the `RedisBroadcastAdapter` handles cross-instance communication. Each API instance maintains its local connection maps while listening to the Redis channel for messages originating from other nodes. When a message arrives from Redis, the adapter routes it to the appropriate local connections based on the message type (project or user scope).

This design allows Kubernetes-deployed Kaneo instances to maintain stateful WebSocket connections while sharing broadcast state through Redis, eliminating the need for sticky sessions.

## Summary

- Kaneo tracks WebSocket connections in two Maps: `projectConnections` for project-wide updates and `userConnections` for personal notifications.
- The `BroadcastAdapter` interface abstracts delivery, allowing the same code to run in single-instance (in-memory) or multi-instance (Redis) deployments.
- Project updates are batched with a 100ms delay to reduce network overhead while preserving event ordering.
- Domain events like `task.created` and `notification.created` automatically trigger WebSocket broadcasts through `broadcastToProject` and `broadcastToUser`.

## Frequently Asked Questions

### How does Kaneo handle WebSocket connections when scaling to multiple API instances?

When Redis is configured, Kaneo initializes the `RedisBroadcastAdapter` in [`apps/api/src/ws/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/index.ts). This adapter publishes messages to a dedicated Redis pub/sub channel, allowing stateless API instances to receive broadcasts from other nodes and deliver them to locally connected clients. Each instance ignores messages tagged with its own unique UUID to prevent echo loops.

### What is the difference between project-scoped and user-scoped WebSocket connections?

Project-scoped connections stored in `projectConnections` receive collaborative updates such as task modifications, status changes, and label assignments. User-scoped connections in `userConnections` carry private, non-collaborative data like system notifications and direct mentions. This separation ensures users only receive relevant data, reducing client-side processing and bandwidth consumption.

### How does Kaneo prevent broadcast storms during rapid task updates?

The `broadcastToProject()` function implements a per-project message queue with a 100ms aggregation window. Rapid successive updates to the same project—such as bulk status changes—are collected into a single batch before transmission. This throttling mechanism reduces duplicate traffic while maintaining the chronological order of events through FIFO queue processing.

### Where does the WebSocket adapter initialization occur in the codebase?

The `initializeWebSocketAdapter()` function resides in [`apps/api/src/ws/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/index.ts) and executes during API startup. It evaluates the Redis configuration status via `isRedisConfigured()`, instantiates the appropriate adapter (in-memory or Redis), and establishes subscriptions to both project-wide and user-wide broadcast channels【^1†L6-L14】.