# How WebSocket Decorators Enable Real-Time Updates in Plane

> Discover how WebSocket decorators in Plane automate real-time updates. Learn how @WebSocket marks handlers for bidirectional communication with Express-compatible routing.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: internals
- Published: 2026-06-18

---

**Plane uses a metadata-driven decorator system where the `@WebSocket` decorator marks class methods as WebSocket handlers, allowing the `registerController` function to automatically discover and register them with an Express-compatible router for bidirectional real-time communication.**

Plane is an open-source project management platform that powers collaborative workspaces with instant synchronization across clients. The application achieves this real-time capability through a lightweight decorator-based architecture defined in the `@plane/decorators` package, which transforms ordinary TypeScript class methods into WebSocket endpoints without mixing low-level socket handling throughout the codebase.

## The @WebSocket Decorator Architecture

The foundation of Plane's real-time system resides in [`packages/decorators/src/websocket.ts`](https://github.com/makeplane/plane/blob/main/packages/decorators/src/websocket.ts). The `@WebSocket` decorator is a method decorator that attaches two critical pieces of metadata to the target method using the Reflect API:

1. **`method = "ws"`** – Signals to the registration logic that this handler operates over WebSocket protocol rather than HTTP.
2. **`route`** – Specifies the URL path segment that clients connect to.

This metadata attachment allows the system to distinguish WebSocket endpoints from traditional REST endpoints at runtime without requiring separate controller classes.

## Controller Registration and Discovery

When a controller class is registered via `registerController` in [`packages/decorators/src/controller.ts`](https://github.com/makeplane/plane/blob/main/packages/decorators/src/controller.ts), the system performs a metadata scan to identify WebSocket-capable methods.

### Metadata Scanning Logic

During registration (lines 31‑36), the helper iterates over the controller's prototype methods and checks for the `method` metadata key. If a method returns `"ws"`, the controller is flagged as a WebSocket controller:

```typescript
// Simplified scanning logic from controller.ts
Object.getOwnPropertyNames(Controller.prototype).forEach(name => {
  if (name === 'constructor') return;
  const methodType = Reflect.getMetadata('method', instance, name);
  if (methodType === 'ws') {
    // Mark for WebSocket registration
  }
});

```

### registerWebSocketController Implementation

The `registerWebSocketController` function (lines 79‑107) handles the actual route binding. It retrieves the controller's `baseRoute` metadata, concatenates it with the method-level route, and invokes `router.ws()` to register the handler with the Express-compatible router:

```typescript
function registerWebSocketController(router, Controller, instance) {
  const base = Reflect.getMetadata('baseRoute', Controller);
  Object.getOwnPropertyNames(Controller.prototype).forEach(name => {
    if (name === 'constructor') return;
    const method = Reflect.getMetadata('method', instance, name);
    const route = Reflect.getMetadata('route', instance, name);
    if (method === 'ws' && route) {
      router.ws(`${base}${route}`, (ws, req) => {
        instance[name].call(instance, ws, req);
      });
    }
  });
}

```

This registration pattern ensures that each new WebSocket connection instantiates the handler with access to both the live `WebSocket` object and the original HTTP request, enabling parameter extraction from URLs (e.g., `req.params.id`).

## Real-Time Capabilities and Error Handling

Once registered, WebSocket decorator methods facilitate real-time updates through bidirectional communication channels. The handler receives the raw WebSocket connection, allowing developers to:

- **Push updates instantly** using `ws.send(JSON.stringify(data))`
- **Listen for client messages** via `ws.on('message', callback)`
- **Access HTTP request context** including authentication headers and URL parameters

### Error Handling and Connection Safety

The decorator system implements robust error isolation (lines 99‑103). Any exception thrown within a WebSocket method is caught, logged, and results in the socket closing with a 1011 status code (internal error):

```typescript
// Error handling pattern from controller.ts lines 99-103
try {
  await instance[methodName].call(instance, ws, req);
} catch (error) {
  console.error(`WebSocket error in ${methodName}:`, error);
  ws.close(1011, 'Internal server error');
}

```

This ensures that server-side failures do not leave connections hanging and clients receive explicit notification of terminal errors.

## Mixed REST and WebSocket Controllers

A key architectural advantage of Plane's WebSocket decorators is the ability to colocate real-time and traditional HTTP endpoints within the same controller class. Because `@WebSocket` merely records metadata, a controller can simultaneously expose REST methods (handled by [`packages/decorators/src/rest.ts`](https://github.com/makeplane/plane/blob/main/packages/decorators/src/rest.ts)) and WebSocket methods.

Dependency injection remains consistent across both protocol types. The `registerController` function creates a single controller instance (line 29) and reuses it for both REST and WebSocket registration:

```typescript
// Instance creation line 29 in controller.ts
const instance = new Controller();

```

This singleton-per-registration pattern ensures that shared service dependencies—such as database connections or message brokers—maintain consistent state regardless of whether a client accesses the controller via HTTP polling or WebSocket subscription.

## Practical Implementation Example

Developers implement real-time features by applying the `@WebSocket` decorator alongside `@BaseRoute` and `@Controller`. The following example demonstrates a room subscription controller that pushes updates to connected clients:

```typescript
import { WebSocket, Controller, BaseRoute } from '@plane/decorators';

@BaseRoute('/api/rooms')
export class RoomController {
  @WebSocket('/:id')
  async join(ws: WebSocket, req: Request) {
    const roomId = req.params.id;
    
    // Confirm subscription to client
    ws.send(JSON.stringify({ message: `Joined room ${roomId}` }));
    
    // Listen for future data changes and broadcast
    ws.on('message', (data) => {
      // Handle incoming client messages
    });
  }
}

```

When the server initializes and calls `registerController(router, RoomController)`, the system discovers the `join` method via its metadata, registers the route `/api/rooms/:id` via `router.ws`, and executes the method for each new socket connection. Clients connecting to `wss://api/rooms/123` immediately enter the real-time communication channel.

## Summary

- **Metadata-driven discovery**: The `@WebSocket` decorator in [`packages/decorators/src/websocket.ts`](https://github.com/makeplane/plane/blob/main/packages/decorators/src/websocket.ts) stores `method: "ws"` and route metadata on class methods.
- **Automatic registration**: `registerController` scans for WebSocket metadata (lines 31‑36) and delegates to `registerWebSocketController` (lines 79‑107) to bind routes.
- **Protocol duality**: Single controller classes can host both REST and WebSocket endpoints with shared dependency injection (line 29).
- **Production safety**: Built-in error handling closes connections with 1011 status codes when exceptions occur (lines 99‑103).
- **Express integration**: The system leverages `router.ws()` from the `ws` library to register handlers compatible with existing Express middleware stacks.

## Frequently Asked Questions

### How does the @WebSocket decorator differ from REST decorators in Plane?

While REST decorators (defined in [`packages/decorators/src/rest.ts`](https://github.com/makeplane/plane/blob/main/packages/decorators/src/rest.ts)) store HTTP-specific metadata like `GET`, `POST`, or `PUT`, the `@WebSocket` decorator stores `method: "ws"` to indicate bidirectional persistent connections. Both use the same `Reflect.defineMetadata` mechanism, allowing `registerController` to scan and categorize endpoints uniformly, but WebSocket handlers receive a `WebSocket` object and `Request` object rather than standard HTTP request/response pairs.

### Can a single controller handle both HTTP and WebSocket connections simultaneously?

Yes. Because decorators only attach metadata without altering method signatures, a controller class can mix `@Get`, `@Post`, and `@WebSocket` decorators on different methods. The `registerController` function creates one instance of the controller (line 29) and processes all metadata, registering REST routes via the standard Express router and WebSocket routes via `router.ws()`. This colocation ensures shared service dependencies remain consistent across both communication protocols.

### What happens when an error occurs inside a WebSocket decorator method?

The `registerWebSocketController` function wraps method execution in a try-catch block (lines 99‑103). If an exception occurs, the error is logged to the console, and the WebSocket connection closes with a 1011 status code indicating an internal server error. This prevents uncaught exceptions from crashing the server while providing clients with a clean disconnect signal.

### How does dependency injection work with WebSocket controllers?

Plane's decorator system instantiates the controller class once during registration (line 29 in [`packages/decorators/src/controller.ts`](https://github.com/makeplane/plane/blob/main/packages/decorators/src/controller.ts)) and reuses that instance for all incoming connections, both HTTP and WebSocket. Constructor-injected services (such as database repositories or event emitters) are initialized at this moment and shared across all requests. This approach ensures that stateful services maintain consistency when pushing real-time updates triggered by HTTP operations in the same controller instance.