Skip to content
cable
Esc
navigateopen⌘Jpreview
On this page

How cable fits together

Connect a shared contract to procedure runtimes, channel hosts, and typed clients.

cable has four pieces. The contract names operations and validates their values. The server implements global procedures and channel behavior. A handler binds those runtimes to requests and upgrades. The client calls the same contract.

Start with the contract

Keep the contract in a module that both sides can import. Global procedures sit in the tree. A channel is a parameterized family with its own events and optional host procedures.

import { c } from "@cablejs/contract";
import { z } from "zod";

export const api = c.contract({
  posts: {
    list: c.query({
      input: z.object({}),
      output: z.object({ ids: z.array(z.string()) }),
    }),
  },
  chat: c.channel("chat.{roomId}", {
    client: {
      send: z.object({ text: z.string().min(1) }),
    },
    server: { message: z.object({ text: z.string() }) },
  }),
});

The contract does not contain handlers or transport setup. It is the value that lets each layer share names, schemas, and inferred input and output types.

Implement global procedures

implement(api) starts a global procedure runtime. Call .context<T>() to describe the request context, then .procedures(...) to supply every global handler. Output validation is enabled by default. Pass { validateOutput: false } only when the handler already guarantees the output at another trusted boundary.

import { implement } from "@cablejs/core";

import { api } from "./api.js";

export const procedures = implement(api)
  .context<{ readonly requestId: string }>()
  .procedures({
    posts: {
      list: () => ({ ids: [] }),
    },
  });

Middleware belongs on a procedure builder when several handlers share a rule. Request authentication and channel grants are separate adapter concerns. See authorization for that boundary.

Bind a handler

For a web-standard HTTP runtime, createRpcHandler accepts the implemented procedures and a context factory. Its default base path is /_cable; it serves procedure POST batches at /_cable/rpc and accepts up to 100 calls or 1 MiB per request unless you provide maxBatchSize, maxBodyBytes, or basePath.

import { createRpcHandler } from "@cablejs/core";

import { procedures } from "./procedures.js";

const rpc = createRpcHandler(procedures, {
  context: (request) => ({
    requestId: request.headers.get("x-request-id") ?? crypto.randomUUID(),
  }),
});

export default { fetch: rpc.fetch };

Node and Cloudflare adapters add channel transports to the same edge routing model. Their public createHandler(contract, procedures, options) functions accept the contract, the global runtime, and adapter options. The required options include the adapter’s credential and grant policy, context factory, and channel host registrations. Read the adapter guide for the environment-specific setup.

Give channels a host

nodeHost(channel, implementation, options?) keeps one channel family in a Node process. Its optional engine value supplies core engine settings. The Node adapter stores channel state in process memory, so a restart drops that state.

cloudflareHost(channel, implementation, options) creates the Durable Object class for one channel family. Its required options are grantSecret and peer; engine is optional. Export the returned class and bind it to a Durable Object namespace. Hibernation lets the host reconstruct runtime state from object storage, socket attachments, and its object name.

Use a global procedure for request-scoped work. Use a channel procedure or event when the operation belongs to one channel’s state, ordering, or membership.

Call the contract from a client

createClient is lazy. It makes no request until a procedure or channel handle is used. Pass contract for typed procedures and channels. url defaults to /_cable; ws, links, fetch, headers, and onError are optional client configuration. The default link batches procedure calls. See Use the client for channels and reconnect behavior.

import { createClient } from "@cablejs/client";

import { api } from "./api.js";

const client = createClient({ contract: api, url: "/_cable" });
const posts = await client.posts.list.query({});
const room = client.chat({ roomId: "general" });
await room.send({ text: "Hello" });

The procedure call goes through the handler’s RPC route. The channel handle opens its socket when the client subscribes, sends an event, or updates presence. Build channels covers channel lifecycle and durable state.

Was this page helpful?