tRPC provides end-to-end type safety for your APIs. RivetKit integrates seamlessly with tRPC, allowing you to create type-safe procedures that call Rivet Actors.

View Example on GitHub

Check out the complete example

Installation

Install tRPC alongside RivetKit:

npm install @trpc/server @trpc/client zod
npm install -D @trpc/next # if using Next.js

Basic Setup

1

Create Your Registry

Set up your Rivet Actors:

// registry.ts
import { actor, setup } from "@rivetkit/actor";

export const counter = actor({
  state: { count: 0 },
  actions: {
    increment: (c, amount: number = 1) => {
      c.state.count += amount;
      c.broadcast("countChanged", c.state.count);
      return c.state.count;
    },
    getCount: (c) => c.state.count,
    reset: (c) => {
      c.state.count = 0;
      c.broadcast("countChanged", 0);
      return 0;
    },
  },
});

export const registry = setup({
  use: { counter },
});
2

Create tRPC Router

Create your tRPC router that uses RivetKit:

// server.ts
import { registry } from "./registry";
import { initTRPC } from "@trpc/server";
import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { z } from "zod";

// Start RivetKit
const { client } = registry.createServer();

// Initialize tRPC
const t = initTRPC.create();

// Create tRPC router with RivetKit integration
const appRouter = t.router({
  // Counter procedures
  counter: t.router({
    increment: t.procedure
      .input(z.object({ 
        name: z.string(),
        amount: z.number().optional().default(1) 
      }))
      .mutation(async ({ input }) => {
        const counter = client.counter.getOrCreate([input.name]);
        const newCount = await counter.increment(input.amount);
        return { name: input.name, count: newCount };
      }),
    
    get: t.procedure
      .input(z.object({ name: z.string() }))
      .query(async ({ input }) => {
        const counter = client.counter.getOrCreate([input.name]);
        const count = await counter.getCount();
        return { name: input.name, count };
      }),
    
    reset: t.procedure
      .input(z.object({ name: z.string() }))
      .mutation(async ({ input }) => {
        const counter = client.counter.getOrCreate([input.name]);
        const count = await counter.reset();
        return { name: input.name, count };
      }),
  }),
});

// Export type for client
export type AppRouter = typeof appRouter;

// Create HTTP server
const server = createHTTPServer({
  router: appRouter,
});

server.listen(3001);
console.log("tRPC server listening at http://localhost:3001");
3

Frontend Client

Create a type-safe tRPC client:

// client.ts
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./server";

export const trpc = createTRPCProxyClient<AppRouter>({
  links: [
    httpBatchLink({
      url: "http://localhost:3001",
    }),
  ],
});

// Usage examples
async function examples() {
  // Increment counter
  const result = await trpc.counter.increment.mutate({ 
    name: "my-counter", 
    amount: 5 
  });
  console.log(result); // { name: "my-counter", count: 5 }
  
  // Get counter value
  const value = await trpc.counter.get.query({ name: "my-counter" });
  console.log(value); // { name: "my-counter", count: 5 }
  
  // Reset counter
  const reset = await trpc.counter.reset.mutate({ name: "my-counter" });
  console.log(reset); // { name: "my-counter", count: 0 }
}