Quickstart
First, create a new empty project:
bun initInstall the packages:
bun add @covenant-rpc/core @covenant-rpc/client @covenant-rpc/server zodCreate three files: covenant.ts, server.ts, and client.ts.
covenant.ts
Section titled “covenant.ts”The covenant is the shared contract between your frontend and backend. It only imports schemas — never backend code.
import { declareCovenant, query, mutation } from "@covenant-rpc/core";import { z } from "zod";
const todoSchema = z.object({ id: z.string(), text: z.string() });
export const covenant = declareCovenant({ procedures: { getTodos: query({ input: z.null(), output: z.array(todoSchema), }), addTodo: mutation({ input: z.object({ text: z.string() }), output: todoSchema, }), }, channels: {},});server.ts
Section titled “server.ts”import { CovenantServer } from "@covenant-rpc/server";import { emptyServerToSidekick } from "@covenant-rpc/server/interfaces/empty";import { covenant } from "./covenant";
const todos: { id: string; text: string }[] = [];
export const server = new CovenantServer(covenant, { contextGenerator: () => undefined, derivation: () => ({}), sidekickConnection: emptyServerToSidekick(),});
server.defineProcedure("getTodos", { resources: () => ["todos"], procedure: () => todos,});
server.defineProcedure("addTodo", { resources: ({ outputs }) => ["todos", `todo/${outputs.id}`], procedure: ({ inputs }) => { const todo = { id: crypto.randomUUID(), text: inputs.text }; todos.push(todo); return todo; },});
server.assertAllDefined();emptyServerToSidekick() disables realtime features. See Sidekick when you’re ready.
client.ts
Section titled “client.ts”import { CovenantClient } from "@covenant-rpc/client";import { httpClientToServer } from "@covenant-rpc/client/interfaces/http";import { emptyClientToSidekick } from "@covenant-rpc/client/interfaces/empty";import { covenant } from "./covenant";
export const client = new CovenantClient(covenant, { serverConnection: httpClientToServer("http://localhost:3000/api/covenant", {}), sidekickConnection: emptyClientToSidekick(),});Calling procedures
Section titled “Calling procedures”import { client } from "./client";
// Query — typed input and outputconst result = await client.query("getTodos", null);if (result.success) { console.log(result.data); // { id: string, text: string }[]}
// Mutation — triggers local cache invalidation after successconst added = await client.mutate("addTodo", { text: "Buy milk" });if (added.success) { console.log(added.data.id); // string}Mounting in a framework
Section titled “Mounting in a framework”Point your framework’s route handler at server.handle(). Here’s Next.js:
import { vanillaAdapter } from "@covenant-rpc/server/adapters/vanilla";import { server } from "@/lib/server";
const handler = vanillaAdapter(server);export { handler as GET, handler as POST };vanillaAdapter turns CovenantServer into (Request) => Response, compatible with any WinterTC-style runtime.