How the Cloudflare OS Workshop Frontend Communicates with the Backend

The Cloudflare OS workshop frontend communicates with its backend through Cap'n Web RPC over a persistent WebSocket connection, using a shared TypeScript API definition for type-safe remote procedure calls.

The cloudflare/cloudflare-os repository implements a full-stack workshop application demonstrating edge computing patterns. The frontend is a single-page React application that maintains a persistent, bidirectional communication channel with the backend worker using Cap'n Proto serialization over WebSockets.

Shared API Contract in packages/workshop-shared/src/api.ts

All communication is governed by the WorkshopApi interface defined in packages/workshop-shared/src/api.ts. This file serves as the single source of truth for both frontend and backend, ensuring that method signatures and data shapes remain synchronized across the stack.

// packages/workshop-shared/src/api.ts
export interface WorkshopApi {
  /** Returns the list of available gadgets */
  listGadgets(): Promise<string[]>;

  /** Creates a new workspace */
  createWorkspace(name: string): Promise<{ id: string }>;

  /** Sends a chat message to the agent */
  sendMessage(workspaceId: string, text: string): Promise<void>;
}

Any changes to the RPC surface must update this shared definition, providing compile-time guarantees that the frontend and backend agree on the protocol.

WebSocket Transport Configuration (packages/workshop-frontend/src/api.ts)

The frontend establishes a binary WebSocket connection to the /api/ws endpoint immediately on application startup. The connection remains open for the lifetime of the page session, eliminating handshake overhead for subsequent RPC calls.

// packages/workshop-frontend/src/api.ts
import { createRpcClient } from '@gadgets/capnweb';
import type { WorkshopApi } from '@gadgets/workshop-shared/api';

const socket = new WebSocket(`${location.origin}/api/ws`);
socket.binaryType = 'arraybuffer';

export const workshopApi: WorkshopApi = createRpcClient<WorkshopApi>(socket);

Setting binaryType to 'arraybuffer' ensures efficient transmission of Cap'n Proto's binary serialization format without additional encoding overhead.

RPC Client Initialization with Cap'n Web

The createRpcClient function from the internal capnweb library constructs a typed client stub from the shared WorkshopApi interface. This stub exposes remote methods as standard async functions while handling message framing, pipelining, and deserialization internally.

Calls made through the stub are automatically serialized into Cap'n Web messages and transmitted via the underlying WebSocket. The backend implementation in packages/workshop-backend/src/rpc.ts exposes corresponding handlers that satisfy the same WorkshopApi interface, ensuring end-to-end type safety.

React Integration and State Management

RPC stubs are not plain JavaScript values and cannot be stored directly in React state without triggering setter function conflicts. The frontend wraps stubs in plain objects to safely persist them within component state.

// packages/workshop-frontend/src/App.tsx
import { useEffect, useState } from 'react';
import { workshopApi } from './api';

export function GadgetList() {
  const [gadgets, setGadgets] = useState<string[]>([]);
  const [apiStub] = useState({ stub: workshopApi });

  useEffect(() => {
    apiStub.stub.listGadgets().then(setGadgets);
  }, [apiStub]);

  return (
    <ul>
      {gadgets.map((g) => (
        <li key={g}>{g}</li>
      ))}
    </ul>
  );
}

This wrapping pattern prevents React from mistaking the stub for a state-updating function while preserving reference stability across renders.

Resource Cleanup and Connection Lifecycle

When components unmount or explicit cleanup is required, the frontend disposes of RPC stubs to release backend resources. Stubs implement the explicit resource management protocol using Symbol.dispose.

// Cleanup pattern in React components
useEffect(() => {
  const stub = workshopApi.createWorkspace("temp");

  return () => {
    // Explicit disposal prevents resource leaks on the backend
    stub[Symbol.dispose]?.();
  };
}, []);

Alternatively, TypeScript's using syntax can manage automatic cleanup when scopes exit, ensuring that transient RPC objects do not accumulate on the server.

Summary

  • Protocol: The frontend uses Cap'n Web RPC over a persistent WebSocket to /api/ws for all backend communication.
  • Type Safety: The shared WorkshopApi interface in packages/workshop-shared/src/api.ts guarantees compile-time synchronization between client and server.
  • Transport: Binary WebSocket connections with arraybuffer type handle serialized Cap'n Proto messages efficiently.
  • React Patterns: RPC stubs are wrapped in plain objects ({ stub }) before storage in React state to avoid setter conflicts.
  • Resource Management: Explicit disposal via Symbol.dispose or using blocks prevents backend resource leaks when components unmount.

Frequently Asked Questions

What protocol does the workshop frontend use to communicate with the backend?

The frontend uses Cap'n Web RPC, a protocol based on Cap'n Proto serialization that runs over a persistent WebSocket connection. This provides type-safe remote procedure calls with lower overhead than HTTP-based REST or GraphQL, as implemented in the cloudflare/cloudflare-os repository.

How is the WebSocket connection configured in the workshop frontend?

In packages/workshop-frontend/src/api.ts, the frontend creates a WebSocket targeting /api/ws and sets socket.binaryType = 'arraybuffer'. This binary mode is critical for transmitting Cap'n Proto's compact binary format without Base64 encoding overhead.

Why are RPC stubs wrapped in plain objects when stored in React state?

RPC stubs are callable objects that React's useState hook may misinterpret as state-updating functions. Wrapping them in plain objects like { stub } prevents the setter from treating the stub as a functional update, ensuring predictable state behavior while preserving the typed client reference.

How does the frontend handle cleanup of RPC connections?

Components dispose of stubs explicitly using stub[Symbol.dispose]() in useEffect cleanup functions or by leveraging TypeScript's using keyword for automatic scope-based cleanup. This signals the backend to release associated resources, preventing memory leaks in long-running sessions.

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 →