tRPC
Routers live in packages/core/src/server/trpc/routers/ and are composed into
the root appRouter. The handler is mounted at /api/trpc/* by
apps/api/src/index.ts, with Sentry fingerprinting per procedure.
Procedure types
Section titled “Procedure types”Defined in packages/core/src/server/trpc/trpc.ts:
| Procedure | Guarantees |
|---|---|
publicProcedure |
No auth. Anything a signed-out visitor may call. |
protectedProcedure |
A signed-in user (enforceUser). ctx.user is non-null. |
roleProtectedProcedure([roles]) |
Signed in and holding one of the roles (enforceRole). Adds ctx.callerRoles. |
Pick the narrowest one that works. Hiding a button is not access control — the procedure is.
The shape of a router
Section titled “The shape of a router”export const thingsRouter = router({ list: publicProcedure .input(z.object({ eventId: z.string().uuid() })) .query(async ({ input }) => { /* ... */ }),
update: roleProtectedProcedure(["admin", "superadmin"]) .input(updateThingSchema) .mutation(async ({ ctx, input }) => { const updated = await db .update(things) .set({ ...input, updatedAt: new Date().toISOString() }) .where(eq(things.id, input.id)) .returning();
await logAudit(ctx, { action: "update", entity: "thing", entityId: input.id, // before/after context });
return updated[0]; }),});Three things that are not optional in a mutation: the Zod input, the manual
updatedAt, and logAudit on every admin create, update and delete.
Adding a router
Section titled “Adding a router”Use the trpc-router skill — it encodes the conventions above plus Sentry capture and registration in the root router. Doing it by hand means:
- New file in
packages/core/src/server/trpc/routers/. - Zod schemas for every input.
- Register it in the root router.
- If mobile needs it, add a
/v1adapter — never a second implementation.
Calling it from the server
Section titled “Calling it from the server”createCaller gives you a typed server-side caller with a context you supply.
That is exactly how the /v1 REST layer reuses the routers; see
REST /v1.
