Let’s allow other services to create an order. Add request and response schemas, declare the capability-gated RPC, then register the handler.

Add the request and response schemas to schemas.ts:

export const CreateOrderRequest = Type.Object({
  customerId: Type.String(),
  items: Type.Array(Type.Object({
    productId: Type.String(),
    quantity: Type.Number({ minimum: 1 }),
  })),
});
export type CreateOrderRequest = Static<typeof CreateOrderRequest>;

export const CreateOrderResponse = Type.Object({
  orderId: Type.String(),
  status: Type.String(),
});
export type CreateOrderResponse = Static<typeof CreateOrderResponse>;

Register the new RPC in contracts/orders_service.ts. Update your imports, change the contract callback to receive ref, declare the local capability metadata in the contract body, and fill in the rpc section:

import * as schemas from "../schemas.ts";

// ...

// In the first defineServiceContract argument:
{ schemas }

// In the returned contract body:
  capabilities: {
    "orders.write": {
      displayName: "Write orders",
      description: "Create order records.",
    },
  },

  rpc: {
    "Orders.Create": {
      version: "v1",
      input: ref.schema("CreateOrderRequest"),
      output: ref.schema("CreateOrderResponse"),
      capabilities: { call: ["orders.write"] },
      errors: [ref.error("ValidationError"), ref.error("UnexpectedError")],
    },
  },

The top-level capabilities map defines what orders.write means for approval and admin UIs. The RPC-level capabilities.call field is the gate: only callers that have been granted the projected orders.write capability can invoke this RPC. Trellis enforces this on every request before your handler ever runs. There is no auth middleware to write.

Now implement the handler in handlers/orders.ts:

import { ok } from "@qlever-llc/trellis";
import type {
  RpcHandlerArgs,
  RpcHandlerResult,
} from "../contracts/orders_service.ts";

type Args = RpcHandlerArgs<"Orders.Create"> & {
  deps: {
    id(): string;
    clock(): Date;
  };
};
type Result = RpcHandlerResult<"Orders.Create">;

export async function createOrderHandler({
  input,
  client,
  deps,
}: Args): Promise<Result> {
  const orderId = deps.id();
  const order = {
    orderId,
    customerId: input.customerId,
    status: "pending" as const,
    items: input.items,
    createdAt: deps.clock().toISOString(),
  };

  await client.kv.orders.put(orderId, order).orThrow();

  return ok({ orderId, status: "pending" });
}

Mount it from main.ts:

import { createOrderHandler } from "./handlers/orders.ts";

await app.handle.rpc.orders.create(createOrderHandler);

Handlers registered through app receive the same Trellis input, context, and client values as unbound handlers, plus the app dependencies you bound in the entrypoint as deps. Handlers return ok(value) on success or err(error) for declared, caller-visible failures. Input validation has already happened by the time your handler is called. If the caller sends a malformed request, Trellis rejects it before reaching your code. Use .orThrow() only for unexpected platform failures that should become UnexpectedError responses.

Review and update deployment authority to include the new RPC boundary, then restart the service.