Call channel procedures
Put per-channel operations beside their channel, then call them over its socket or the authenticated HTTP fallback.
Global procedures live in implement(api).procedures(...). Channel procedures belong to the procedures map inside c.channel(...) because they execute in one resolved channel host. The following continuation belongs in the api.ts channel definition from Build channels.
import { c } from "@cablejs/contract";
import { z } from "zod";
// api.ts
export const api = c.contract({
chat: c.channel("chat.{roomId}", {
client: {},
server: { message: z.object({ text: z.string() }) },
procedures: {
recent: c.query({
input: z.object({ limit: z.number().int().min(1).max(100) }),
output: z.array(z.object({ text: z.string() })),
errors: { FORBIDDEN: z.void() },
}),
},
}),
});
import { CableError, type ChannelImplementation } from "@cablejs/core";
import { api } from "./api.js";
interface Identity {
readonly userId: string;
}
export const chatImplementation = {
onClient: {},
procedures: {
async recent(context, { limit }) {
if (!context.grants.includes(`room:${context.params.roomId}`)) {
throw new CableError("FORBIDDEN");
}
return Array.from(
(await context.storage.list<{ text: string }>({ prefix: "message:", limit })).values(),
);
},
},
} satisfies ChannelImplementation<typeof api.chat, Identity>;
The handler receives parsed input plus the host’s parsed params, authenticated identity, grants, namespaced storage, peers, and event methods. A socket call also has connection; an HTTP fallback does not, so treat it as optional.
// client.ts
const room = client.chat({ roomId: "general" });
const messages = await room.recent({ limit: 50 });
cable sends this call through an open channel socket when one exists. The edge handler also exposes POST /_cable/host/<channel-key>/<procedure> for an authenticated caller without a socket. Both paths validate the same procedure input and invoke the same host implementation.
Use a channel procedure when the operation needs one channel’s state, ordering, or membership. Use a global procedure for ordinary request-scoped work. Neither kind inherits authorization from the other: the edge authenticates every request, cable signs channel grants, and the channel still decides what its identity and grants may do.
history.load is reserved by cable when a channel declares history. Do not declare an application procedure with that name.