How the Cap'n Web RPC Interface Ensures Runtime Type Validation in Cloudflare OS

Cloudflare OS uses the capnweb-validate build-time transformer to inject runtime type-checking code into every Cap'n Web RPC method, ensuring that only well-formed data crosses the network boundary.

Cloudflare OS leverages the Cap'n Web RPC framework to handle communication between front-end and back-end services. To prevent type mismatches that could corrupt application state or create security vulnerabilities, the cloudflare/cloudflare-os repository implements a rigorous validation pipeline that bridges static TypeScript types with runtime enforcement. This article examines how the system guarantees runtime type validation through compile-time transformations and consumer-side enforcement mechanisms.

The TypeScript-to-Runtime Validation Pipeline

The validation strategy combines static TypeScript definitions with generated runtime checks. This pipeline ensures that type safety extends beyond compilation to actual execution across network boundaries.

Typed RPC Interface Definitions

All public RPC interfaces are defined as TypeScript interfaces extending RpcTarget. These declarations specify exact argument types and return shapes, creating a strict contract between client and server. In packages/workshop-shared/src/api.ts (lines 60-69), the PublicApi interface declares methods like startGatekeeperLogin with precise signatures:

export interface PublicApi extends RpcTarget {
  startGatekeeperLogin(vendorId: string): Promise<{
    url: string;
    attempt: RpcStub<LoginAttempt>;
  }>;
}

The @validateRpc() Decorator

Server implementations apply the @validateRpc() decorator imported from the capnweb-validate package. This decorator functions as a compile-time marker that signals the build tool to generate validation logic. In packages/workshop-backend/src/server.ts (lines 2-75), the decorator is applied to classes implementing RPC interfaces:

import { validateRpc } from "capnweb-validate";

@validateRpc()
export class PublicApiImpl implements PublicApi {
  async startGatekeeperLogin(vendorId: string) {
    // Implementation logic here
    const { url, attempt } = await this._gatekeeper.startLogin(vendorId);
    return { url, attempt };
  }
}

The decorator itself is a no-op at runtime; its sole purpose is to trigger the transformation process during the build phase.

Build-Time Code Generation

During the worker build process, the capnweb-validate build command parses TypeScript declarations and emits wrapper code. This transformer examines every method signature in the RPC interface and generates corresponding validation logic that checks nested objects, arrays, and enums. The build script in packages/workshop-backend/package.json executes:

pnpm exec capnweb-validate build --out .wrangler/validate

The transformer outputs a deterministic validation bundle to .wrangler/validate, which contains wrappers that verify incoming arguments and return values against the declared schemas before and after method execution.

Runtime Enforcement and Bundle Loading

Once built, the validation bundle integrates automatically into the worker runtime, creating an enforcement layer that protects application logic.

Automatic Validation Bundle Integration

The Vite configuration in packages/workshop-backend/vite.config.ts (lines 49-56) ensures the generated validation bundle loads automatically when the worker starts. This configuration imports the .wrangler/validate directory, making the runtime checks available without manual intervention.

When a client invokes an RPC method, the generated wrapper executes first. If any argument fails type validation—including complex nested structures—the wrapper throws a descriptive Error before application logic runs. This guarantees that only well-formed data reaches the server and that responses respect the declared contract.

Selective Validation Control

For performance-critical paths where callers are fully trusted, developers can bypass validation while maintaining the decorator infrastructure.

Bypassing Checks with skipRpcValidation()

The capnweb-validate package exports skipRpcValidation(), a decorator that disables runtime checks for specific methods. This pattern appears in gatekeeper implementations like packages/gatekeeper-zoominfo/src/zoominfo.ts (lines 2-4), where trusted internal calls require maximum performance:

import { skipRpcValidation } from "capnweb-validate";

export class ZoomInfoGatekeeper {
  @skipRpcValidation()
  async fastInternalCall() {
    // Validation bypassed for trusted callers
    return this.processData();
  }
}

Consumer-Side Validation Design

The architecture intentionally places validation logic in the consumer rather than the provider. According to the design documentation in plans/gatekeeper-kit.md (lines 2123-2146), this choice ensures that interface changes are caught immediately at call sites rather than propagating through the system. The @validateRpc() decorator stays with the consumer code, and capnweb-validate validates every RPC call against the generated schema before transmission, preventing invalid data from crossing network boundaries.

Summary

  • TypeScript interfaces in packages/workshop-shared/src/api.ts define strict RPC contracts extending RpcTarget, specifying exact argument and return types.
  • The @validateRpc() decorator triggers build-time generation of validation code via the capnweb-validate package.
  • The build process emits a validation bundle to .wrangler/validate through the capnweb-validate build command, integrated via Vite configuration.
  • Runtime wrappers enforce type checking on arguments and return values before executing application logic, throwing descriptive errors on mismatch.
  • skipRpcValidation() allows selective disabling of checks for trusted, performance-critical methods in gatekeeper implementations.
  • Consumer-side placement ensures type mismatches are caught at the call site, maintaining strong contracts across the network boundary.

Frequently Asked Questions

What happens if an RPC call fails type validation at runtime?

The generated wrapper throws a descriptive Error immediately, aborting the call before any application logic executes. This prevents malformed data from reaching server-side handlers or returning to clients, ensuring that only valid data crosses the network boundary.

Why is the validation logic placed in the consumer rather than the provider?

Placing validation in the consumer ensures that any changes to the RPC interface are caught at the call site immediately. As documented in plans/gatekeeper-kit.md, this design prevents silent failures and ensures that the calling code respects the declared contract before data transmission occurs.

Can I disable validation for specific methods while keeping it for others?

Yes. Import skipRpcValidation from capnweb-validate and apply it as a decorator to specific methods. This pattern is used in gatekeeper implementations like packages/gatekeeper-zoominfo/src/zoominfo.ts to bypass checks for trusted internal calls while maintaining validation across the rest of the API surface.

How does the build process know which classes need validation?

The @validateRpc() decorator serves as a compile-time marker. When capnweb-validate build runs, it scans for these decorators and generates corresponding validation wrappers for the decorated classes and their methods, as implemented in packages/workshop-backend/src/server.ts.

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 →