Circle V2 API Docs
    Preparing search index...

    tRPC

    The API layer uses tRPC v11 integrated with TanStack React Query. All tRPC code lives in packages/trpc.

    %%{init:{"theme":"dark"}}%% graph LR subgraph client [Client - apps/web] Hooks["useTRPC() + useQuery/useMutation"] end

    subgraph server [Server - packages/trpc] Adapter["Fetch Adapter<br/>(/api/trpc)"] Router["App Router"] Procedures["Procedures<br/>(public, authenticated)"] Middleware["Middleware<br/>(access checks)"] end

    subgraph data [Data - packages/db] Repos["Repositories"] end

    Hooks -->|HTTP batch| Adapter Adapter --> Router Router --> Procedures Procedures --> Middleware Middleware --> Repos

    %%{init:{"theme":"default"}}%% graph LR subgraph client [Client - apps/web] Hooks["useTRPC() + useQuery/useMutation"] end

    subgraph server [Server - packages/trpc] Adapter["Fetch Adapter<br/>(/api/trpc)"] Router["App Router"] Procedures["Procedures<br/>(public, authenticated)"] Middleware["Middleware<br/>(access checks)"] end

    subgraph data [Data - packages/db] Repos["Repositories"] end

    Hooks -->|HTTP batch| Adapter Adapter --> Router Router --> Procedures Procedures --> Middleware Middleware --> Repos

    graph LR
    subgraph client [Client - apps/web]
    Hooks["useTRPC() + useQuery/useMutation"]
    end

    subgraph server [Server - packages/trpc] Adapter["Fetch Adapter<br/>(/api/trpc)"] Router["App Router"] Procedures["Procedures<br/>(public, authenticated)"] Middleware["Middleware<br/>(access checks)"] end

    subgraph data [Data - packages/db] Repos["Repositories"] end

    Hooks -->|HTTP batch| Adapter Adapter --> Router Router --> Procedures Procedures --> Middleware Middleware --> Repos

    The app router composes domain-specific routers:

    // packages/trpc/src/routers/app.router.ts
    export const appRouter = trpc.router({
    patients: patientsRouter,
    patientSessions: patientSessionsRouter,
    profiles: profilesRouter,
    orgMembers: orgMembersRouter,
    facilities: facilitiesRouter,
    templates: templatesRouter,
    auditTemplates: auditTemplatesRouter,
    });

    Each domain router lives in its own folder under packages/trpc/src/routers/:

    packages/trpc/src/routers/
    ├── app.router.ts # Root router composition
    ├── patients/
    │ ├── index.ts # Composes the patients router
    │ ├── list.query.ts
    │ ├── get-by-id.query.ts
    │ ├── create.mutation.ts
    │ └── update.mutation.ts
    ├── audit-templates/
    │ ├── index.ts
    │ ├── get-by-id.query.ts
    │ ├── create.mutation.ts
    │ ├── update.mutation.ts
    │ └── delete.mutation.ts
    └── ...

    Each operation is a separate file named <operation>.<query|mutation>.ts:

    • list.query.ts -- list resources
    • get-by-id.query.ts -- fetch a single resource
    • create.mutation.ts -- create a resource
    • update.mutation.ts -- update a resource
    • delete.mutation.ts -- delete a resource

    Procedures define the authentication/authorization layers. They are defined in packages/trpc/src/procedures.ts:

    // No auth required
    export const publicProcedure = trpc.procedure;

    // Requires a logged-in user (throws UNAUTHORIZED otherwise)
    export const authenticatedProcedure = trpc.procedure.use(async function isAuthenticated({ ctx, next }) {
    if (!ctx.user) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
    }
    return next({ ctx: { ...ctx, user: ctx.user } });
    });

    // Requires a specific role (extends authenticatedProcedure)
    export const authorizedProcedure = (allowedRoles: unknown[]) => {
    return authenticatedProcedure.use(async function hasAllowedRole({ ctx, next }) {
    // Role checking (TODO: re-implement)
    return next({ ctx: { ...ctx, user: ctx.user } });
    });
    };

    Most procedures use authenticatedProcedure as their base.

    The tRPC context is intentionally minimal:

    // packages/trpc/src/context.ts
    export type TRPCContext = {
    user: User | null;
    };

    export type AuthenticatedTRPCContext = SetNonNullable<TRPCContext, "user">;

    The database is not in the context. Repos are imported directly where needed. The context is created per-request in the web app:

    // apps/web/lib/trpc.context.ts
    export async function createTRPCContext() {
    const user = await getLoggedInUser();
    return { user };
    }

    For resource-level access checks, we use a middleware pattern that loads the resource and verifies access before the procedure runs:

    // packages/trpc/src/middleware/auth/can-access-audit-template.middleware.ts
    export function canAccessAuditTemplate({ getId }: CanAccessAuditTemplateOptions) {
    return authenticatedProcedure.use(async (opts) => {
    const input = opts.input as Record<string, unknown>;
    const id = getId(input);
    const { data: template, error } = await auditTemplatesRepo.getById(id);

    if (error) {
    throw new TRPCError({ code: "NOT_FOUND", message: "Audit template not found" });
    }

    if (!hasAccessToAuditTemplate(template, opts.ctx.user)) {
    throw new TRPCError({ code: "FORBIDDEN" });
    }

    return opts.next({ ctx: { auditTemplate: template } });
    });
    }

    This middleware is composed into procedures using .concat():

    // packages/trpc/src/routers/audit-templates/get-by-id.query.ts
    export const getById = authenticatedProcedure
    .input(getAuditTemplateByIdInputSchema)
    .concat(canAccessAuditTemplate({ getId: (input) => input.id as AuditTemplateId }))
    .query(async ({ input, ctx }) => {
    // ctx.auditTemplate is now available (added by middleware)
    const { data: questions, error } = await auditQuestionsRepo.listByTemplateId(input.id);

    if (error) {
    throw new TRPCError({ code: "BAD_REQUEST", message: questionsError.message || "Unknown error" });
    }

    return { questions, auditTemplate: ctx.auditTemplate };
    });

    Inputs use Zod schemas with branded ID validators from @repo/db/schema:

    import { zAuditTemplateId } from "@repo/db/schema";

    export const getAuditTemplateByIdInputSchema = z.object({
    id: zAuditTemplateId,
    });

    export type GetAuditTemplateByIdInput = z.infer<typeof getAuditTemplateByIdInputSchema>;

    Export input/output types so consumers (tests, other packages) can use them.

    The tRPC client is provided via TRPCProvider in apps/web/lib/providers.tsx. It uses an HTTP batch link pointing to /api/trpc.

    The preferred pattern is useTRPC() + .queryOptions() with useQuery:

    import { useQuery } from "@repo/trpc/client";

    function PatientList() {
    const trpc = useTRPC();

    const { data, isLoading } = useQuery(
    trpc.patients.list.queryOptions({ entityId })
    );
    // ...
    }
    import { useMutation, useTRPC, useTRPCClient } from "@repo/trpc/client";

    function CreatePatient() {
    const trpc = useTRPC();
    const queryClient = useQueryClient();

    const mutation = useMutation(
    trpc.patients.create.mutationOptions({
    onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: trpc.patients.list.queryKey() });
    },
    })
    );

    // mutation.mutate({ ... });
    }

    @repo/trpc/client re-exports everything you need:

    Export Source Purpose
    useTRPC tRPC React Query Access procedure options
    TRPCProvider tRPC React Query Provider component
    useQuery, useMutation, useInfiniteQuery @repo/react-query React Query hooks
    useTRPCClient tRPC React Query Direct client access

    The fetch adapter at apps/web/app/api/trpc/[trpc]/route.ts connects the Next.js route handler to the tRPC router:

    import { createTRPCFetchAdapter } from "@repo/trpc/server";
    import { createTRPCContext } from "~/lib/trpc.context";

    const handler = createTRPCFetchAdapter({ createContext: createTRPCContext });

    export { handler as GET, handler as POST };
    1. Create a file in the appropriate router folder (e.g. packages/trpc/src/routers/patients/archive.mutation.ts).
    2. Define the Zod input schema and export its type.
    3. Build the procedure using authenticatedProcedure.input(schema).mutation(...).
    4. Add the procedure to the domain router's index.ts.
    5. The procedure is automatically available on the client via trpc.patients.archive.

    Next: Error Handling