Harness-engineering trpc-context-pattern

tRPC Context Pattern

install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/trpc-context-pattern" ~/.claude/skills/intense-visions-harness-engineering-trpc-context-pattern-1a5332 && rm -rf "$T"
manifest: agents/skills/codex/trpc-context-pattern/SKILL.md
source content

tRPC Context Pattern

Inject database clients, sessions, and request data into every procedure via

createTRPCContext

When to Use

  • Setting up a tRPC server that needs database access in procedures
  • Injecting the authenticated user's session into the context so middleware and procedures can read it
  • Passing request headers or IP addresses to procedures for logging, rate limiting, or locale detection
  • Structuring the context type so TypeScript understands what is available in every procedure handler

Instructions

1. Define the context type and factory

// server/context.ts
import { type inferAsyncReturnType } from '@trpc/server';
import { type FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { db } from '@/lib/db';

export async function createTRPCContext({ req }: FetchCreateContextFnOptions) {
  const session = await getServerSession(authOptions);

  return {
    db,
    session,
    headers: req.headers,
  };
}

// Infer the Context type from the factory — use this throughout the codebase
export type Context = inferAsyncReturnType<typeof createTRPCContext>;

2. Pass context to initTRPC

// server/trpc.ts
import { initTRPC } from '@trpc/server';
import { type Context } from './context';
import superjson from 'superjson';

const t = initTRPC.context<Context>().create({
  transformer: superjson,
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const createCallerFactory = t.createCallerFactory;

3. Wire the context factory to the adapter

// app/api/trpc/[trpc]/route.ts — Next.js App Router
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/root';
import { createTRPCContext } from '@/server/context';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: createTRPCContext,
  });

export { handler as GET, handler as POST };

4. Access context in procedures and middleware

// server/routers/post.ts
import { router, publicProcedure, protectedProcedure } from '../trpc';

export const postRouter = router({
  list: publicProcedure.query(({ ctx }) => {
    // ctx.db is typed as PrismaClient
    // ctx.session is typed as Session | null
    return ctx.db.post.findMany({
      where: { published: true },
    });
  }),

  myPosts: protectedProcedure.query(({ ctx }) => {
    // After isAuthed middleware, ctx.session is Session (non-null)
    return ctx.db.post.findMany({
      where: { authorId: ctx.session.user.id },
    });
  }),
});

5. Extend context in middleware

Middleware can add properties to the context using

next({ ctx: { ...ctx, extra } })
:

// server/trpc.ts
import { TRPCError } from '@trpc/server';

const isAuthed = t.middleware(async ({ ctx, next }) => {
  if (!ctx.session?.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      // TypeScript narrows: session and user are guaranteed non-null downstream
      session: ctx.session,
      user: ctx.session.user,
    },
  });
});

export const protectedProcedure = t.procedure.use(isAuthed);

6. Create context for server-side callers (RSC)

// lib/trpc/server.ts — for React Server Components
import { cache } from 'react';
import { headers } from 'next/headers';
import { createCallerFactory } from '@/server/trpc';
import { appRouter } from '@/server/root';
import { createTRPCContext } from '@/server/context';

// React.cache() scopes context to one instance per request
const createContext = cache(async () => {
  const headerStore = await headers();
  return createTRPCContext({
    req: new Request('http://internal', { headers: headerStore }),
  } as any);
});

const createCaller = createCallerFactory(appRouter);
export const api = createCaller(createContext);

Details

inferAsyncReturnType
for the Context type. Instead of manually defining the
Context
type, derive it from
createTRPCContext
using
inferAsyncReturnType<typeof createTRPCContext>
. This ensures the type stays in sync with the factory function — add a field to the factory and it automatically appears in
ctx
everywhere.

Context creation runs per request.

createTRPCContext
is called once per HTTP request (or once per WebSocket connection for subscriptions). Put expensive operations that are needed for every procedure here. Avoid per-procedure lookups that should be shared (e.g., loading the user's organization).

Database connection patterns:

  • Prisma: import a singleton
    db
    instance and include it in context. Prisma manages the connection pool internally.
  • Drizzle: same pattern — import the
    db
    instance.
  • Raw pg/mysql2: create a connection per request or use a pool; return the client in context and close it after the request (use
    responseMeta
    or adapter lifecycle hooks).

Context vs middleware for auth. The context factory can include the session (as shown above), but it should NOT throw if the session is absent — some procedures are public. Use middleware to enforce auth on specific procedures. The context factory just makes data available; middleware enforces access control.

Testing with custom context. For unit tests, create a caller with a mock context using

createCallerFactory
:

const caller = createCallerFactory(appRouter)({
  db: mockDb,
  session: { user: { id: 'test-user', role: 'user' } },
  headers: new Headers(),
});

const result = await caller.post.list({});

This bypasses HTTP entirely — pure function call with typed inputs and outputs.

Source

https://trpc.io/docs/server/context

Process

  1. Read the instructions and examples in this document.
  2. Apply the patterns to your implementation, adapting to your specific context.
  3. Verify your implementation against the details and edge cases listed above.

Harness Integration

  • Type: knowledge — this skill is a reference document, not a procedural workflow.
  • No tools or state — consumed as context by other skills and agents.

Success Criteria

  • The patterns described in this document are applied correctly in the implementation.
  • Edge cases and anti-patterns listed in this document are avoided.