Claude Skills Guide

Real-time applications demand robust communication channels, and tRPC with WebSocket subscriptions offers a type-safe solution for building interactive features. This guide shows you how to use Claude Code to streamline the entire tRPC WebSocket workflow—from initial setup to production deployment.

Understanding tRPC WebSocket Subscriptions

tRPC’s request-response model works well for most API calls, but real-time features require a different approach. WebSocket subscriptions enable bidirectional communication where the server pushes updates to clients without repeated requests. This is essential for live dashboards, collaborative editing, notification systems, and gaming features.

The tRPC ecosystem provides @trpc/server/adapters/ws for WebSocket handling, creating a persistent connection between client and server. Unlike HTTP, this connection stays open, allowing instant data transfer in both directions.

Setting Up Your tRPC WebSocket Project

Begin by creating a new project or adding WebSocket support to your existing tRPC setup. Claude Code can scaffold this efficiently by understanding your current stack and generating appropriate configuration.

First, ensure you have the required dependencies:

npm install @trpc/server @trpc/client ws zod

Next, configure your tRPC router to support subscriptions. Create a dedicated router for WebSocket procedures:

import { initTRPC } from '@trpc/server';
import { observable } from '@trpc/server/observable';
import { EventEmitter } from 'events';

const ee = new EventEmitter();

export const t = initTRPC.create();

export const appRouter = t.router({
  onMessage: t.procedure.subscription(() => {
    return observable((emit) => {
      const onMessage = (data: string) => {
        emit.next({ message: data });
      };
      ee.on('message', onMessage);
      return () => {
        ee.off('message', onMessage);
      };
    });
  }),
});

export type AppRouter = typeof appRouter;

This pattern uses RxJS observables to stream data to connected clients. The observable function from tRPC handles the subscription lifecycle, including connection establishment and cleanup on disconnect.

Implementing Real-Time Procedures with Claude Code

Claude Code excels at generating repetitive boilerplate for subscription handlers. When you need to subscribe to database changes, API updates, or custom events, describe your requirements and let Claude Code generate the implementation.

For example, a live notification system might look like:

import { z } from 'zod';

export const appRouter = t.router({
  // Standard procedure for sending notifications
  sendNotification: t.procedure
    .input(z.object({
      userId: z.string(),
      content: z.string(),
      type: z.enum(['info', 'warning', 'error']),
    }))
    .mutation(async ({ input }) => {
      const notification = await db.notification.create({
        data: input,
      });
      // Emit event to subscribed clients
      ee.emit('notification', notification);
      return notification;
    }),

  // Subscription for receiving notifications
  onNotification: t.procedure
    .input(z.object({ userId: z.string() }))
    .subscription(({ input }) => {
      return observable((emit) => {
        const onNotification = (notification: Notification) => {
          if (notification.userId === input.userId) {
            emit.next(notification);
          }
        };
        ee.on('notification', onNotification);
        return () => {
          ee.off('notification', onNotification);
        };
      });
    }),
});

The key insight here is using an EventEmitter to bridge mutations and subscriptions. When a notification is created, the mutation emits an event that triggers all matching subscriptions.

Client-Side Subscription Implementation

On the client side, you’ll use @trpc/client with the WebSocket link to connect to your subscription endpoints. Here’s a practical client setup:

import { createTRPCWebSocketLink } from '@trpc/client/links/wsLink';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';

const wsLink = createTRPCWebSocketLink<AppRouter>({
  url: 'ws://localhost:3000/trpc',
});

const trpc = createTRPCClient<AppRouter>({
  links: [wsLink],
});

// Subscribing to notifications
const subscription = trpc.onNotification.subscribe(
  { userId: 'user-123' },
  {
    onData: (notification) => {
      console.log('New notification:', notification);
      // Update UI, show toast, etc.
    },
    onError: (error) => {
      console.error('Subscription error:', error);
    },
  }
);

// Cleanup when done
subscription.unsubscribe();

This client code establishes a persistent WebSocket connection and listens for push notifications. The onData callback fires whenever the server emits a matching notification.

Handling Connection Lifecycle

Production applications must handle network interruptions gracefully. WebSocket connections can drop due to network changes, server restarts, or timeout issues. Implement reconnection logic to maintain reliable real-time features.

Here’s a robust reconnection pattern:

class TRPCWebSocketManager {
  private client: TRPCWebSocketClient;
  private reconnectAttempts = 0;
  private maxReconnectAttempts = 5;
  private reconnectDelay = 1000;

  async connect() {
    try {
      await this.client.connect();
      this.reconnectAttempts = 0;
    } catch (error) {
      await this.handleReconnection();
    }
  }

  private async handleReconnection() {
    if (this.reconnectAttempts >= this.maxReconnectAttempts) {
      console.error('Max reconnection attempts reached');
      return;
    }

    this.reconnectAttempts++;
    const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
    
    await new Promise(resolve => setTimeout(resolve, delay));
    await this.connect();
  }
}

Exponential backoff prevents overwhelming the server during outages while ensuring quick recovery when issues resolve.

Optimizing Performance for High-Volume Subscriptions

When handling numerous concurrent subscriptions, consider implementing connection pooling and message batching. Each WebSocket connection consumes server resources, so optimize carefully.

Use Redis or another pub/sub system for scaling across multiple server instances:

import Redis from 'ioredis';

const redis = new Redis();

export const createCrossServerSubscription = (channel: string) => {
  return observable((emit) => {
    const subscriber = new Redis();
    
    subscriber.subscribe(channel);
    subscriber.on('message', (ch, message) => {
      if (ch === channel) {
        emit.next(JSON.parse(message));
      }
    });

    return () => {
      subscriber.unsubscribe(channel);
      subscriber.disconnect();
    };
  });
};

This approach allows multiple server instances to share subscription events, enabling horizontal scaling while maintaining real-time capabilities.

Testing Your WebSocket Workflows

Comprehensive testing ensures your real-time features work reliably. Test both the happy path and failure scenarios including connection drops, message delivery failures, and concurrent updates.

Use tools like ws for server-side testing and client-side libraries that simulate WebSocket behavior:

import { WebSocketServer } from 'ws';
import { applyWSSHandler } from '@trpc/server/adapters/ws';

const wss = new WebSocketServer({ port: 3000 });
const handler = applyWSSHandler({ wss, router: appRouter, createContext });

// Test connection
const client = new WebSocket('ws://localhost:3000/trpc');
client.on('open', () => {
  console.log('Connected to WebSocket server');
});

// Test cleanup
process.on('SIGTERM', () => {
  console.log('SIGTERM');
  handler.broadcastReconnectNotification();
  wss.close();
});

Conclusion

Building real-time applications with tRPC and WebSockets requires understanding subscription patterns, connection lifecycle management, and production scaling strategies. Claude Code accelerates development by generating boilerplate code, suggesting optimizations, and helping debug complex async flows.

Start with basic subscriptions, then layer in reconnection logic, cross-server communication, and performance optimizations as your application grows. The type-safe foundation tRPC provides makes this evolution manageable and maintainable.

Built by theluckystrike — More at zovo.one