ProvenPin Install

tRPC 11 context, auth middleware and createCaller tests: working versions (tested 2026-10-15)

tRPC 11 router with a request context, a protectedProcedure built from middleware, Zod inputs and TRPCError codes, tested with createCaller (no HTTP).

passed4 passed · 0 failed2026-10-15 13:25 UTC
Status
passed
Last passed
2026-10-15 13:25 UTC
Tests
4 / 4
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
@trpc/server@11.19.0zod@4.6.7tsx@4.23.15@types/node@24.19.2
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38741369695

The code

Exactly the files that passed. Your agent can pull them with get_recipe("trpc-context-auth-middleware") (install), or see them on GitHub.

package.json
{
  "name": "trpc-context-auth-middleware",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "@trpc/server": "11.19.0",
    "zod": "4.6.7"
  },
  "devDependencies": {
    "tsx": "4.23.15",
    "@types/node": "24.19.2"
  }
}
src/router.ts
import { initTRPC, TRPCError } from "@trpc/server";
import { z } from "zod";

export type Context = { user: { id: string; role: "admin" | "user" } | null };
const t = initTRPC.context<Context>().create();

const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
  return next({ ctx: { user: ctx.user } }); // narrows user to non-null downstream
});
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
  if (ctx.user.role !== "admin") throw new TRPCError({ code: "FORBIDDEN" });
  return next();
});

const notes: { id: number; owner: string; text: string }[] = [];

export const appRouter = t.router({
  health: publicProcedure.query(() => "ok"),
  addNote: protectedProcedure.input(z.object({ text: z.string().min(1).max(280) })).mutation(({ ctx, input }) => {
    const note = { id: notes.length + 1, owner: ctx.user.id, text: input.text };
    notes.push(note);
    return note;
  }),
  myNotes: protectedProcedure.query(({ ctx }) => notes.filter((n) => n.owner === ctx.user.id)),
  allNotes: adminProcedure.query(() => notes),
});
export type AppRouter = typeof appRouter;
export const createCaller = t.createCallerFactory(appRouter);
test/router.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { TRPCError } from "@trpc/server";
import { createCaller } from "../src/router.ts";

const anon = createCaller({ user: null });
const ada = createCaller({ user: { id: "ada", role: "user" } });
const root = createCaller({ user: { id: "root", role: "admin" } });
const code = (c: string) => (e: unknown) => e instanceof TRPCError && e.code === c;

test("public works without a user", async () => {
  assert.equal(await anon.health(), "ok");
});

test("protected requires a user", async () => {
  await assert.rejects(anon.addNote({ text: "x" }), code("UNAUTHORIZED"));
  const n = await ada.addNote({ text: "hi" });
  assert.equal(n.owner, "ada");
  assert.equal((await ada.myNotes()).length, 1);
});

test("zod input errors become BAD_REQUEST", async () => {
  await assert.rejects(ada.addNote({ text: "" }), code("BAD_REQUEST"));
});

test("admin-only procedure", async () => {
  await assert.rejects(ada.allNotes(), code("FORBIDDEN"));
  assert.ok(Array.isArray(await root.allNotes()));
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.