I was working on a nextjs monorepo using trpc as backend I am integrating SSE with normal trpc methods, I implemented auth using better auth and it works very smoothly, but when I try to prefetch on server components I get not authenticated user error, but after render the app works without error. I checked further and reached upon conclusion that cookies arent resolved because when I tried passing headers vis prop drilling from TRPC provider and using those in getting auth it worked and prefetching worked here is the code.
// init.ts
import { initTRPC, TRPCError } from '@trpc/server';
import superjson from "superjson";
import { cache } from 'react';
import { auth } from '../lib/auth';
import { headers as getHeaders } from 'next/headers';
export const createTRPCContext = cache(async () => { });
export type Context = Awaited<ReturnType<typeof createTRPCContext>>;
const t = initTRPC.context().create({
transformer: superjson,
errorFormatter({ shape }) {
return shape;
},
sse: {
maxDurationMs: 5 * 60 * 1_000,
ping: {
enabled: true,
intervalMs: 3_000,
},
client: {
reconnectAfterInactivityMs: 5_000,
},
},
});
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;
export const protectedProcedure = baseProcedure.use(async ({ ctx, next }) => {
const headers = await getHeaders();
const session = await auth.api.getSession({
headers
});
if (!session || !session.user) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You must be logged in to access this resource",
});
}
return next({
ctx: {
...ctx,
session,
},
});
});
// server.tsx
import 'server-only';
import { createTRPCOptionsProxy, TRPCQueryOptions } from '@trpc/tanstack-react-query';
import { cache } from 'react';
import { makeQueryClient } from './query-client';
import { appRouter } from './routers/_app';
import { dehydrate, HydrationBoundary } from '@tanstack/react-query';
import { createTRPCContext } from './init';
export const getQueryClient = cache(makeQueryClient);
export const trpc = createTRPCOptionsProxy({
ctx: createTRPCContext,
router: appRouter,
queryClient: getQueryClient,
});
export function HydrateClient(props: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<HydrationBoundary state={dehydrate(queryClient)}>
{props.children}
</HydrationBoundary>
);
}
export function prefetch<T extends ReturnType<TRPCQueryOptions<any>>>(
queryOptions: T,
) {
const queryClient = getQueryClient();
if (queryOptions.queryKey[1]?.type === 'infinite') {
void queryClient.prefetchInfiniteQuery(queryOptions as any);
} else {
void queryClient.prefetchQuery(queryOptions);
}
}
// route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@workspace/trpc/server/routers/_app";
import { createTRPCContext } from "@workspace/trpc/server/init";
const handler = async (req: Request) => {
return fetchRequestHandler({
endpoint: "/api/trpc",
req,
router: appRouter,
createContext: createTRPCContext,
});
};
export { handler as GET, handler as POST };
// client.tsx
"use client";
import type { QueryClient } from "@tanstack/react-query";
import { QueryClientProvider } from "@tanstack/react-query";
import {
createTRPCClient,
httpSubscriptionLink,
httpBatchLink,
httpLink,
splitLink,
loggerLink,
isNonJsonSerializable,
type TRPCLink,
} from "@trpc/client";
import { createTRPCContext } from "@trpc/tanstack-react-query";
import { useState } from "react";
import { makeQueryClient } from "@workspace/trpc/server/query-client";
import type { AppRouter } from "@workspace/trpc/server/routers/_app";
import superjson from "superjson";
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
let browserQueryClient: QueryClient;
function getQueryClient(): QueryClient {
if (typeof window === "undefined") return makeQueryClient();
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
function getUrl() {
const base = (() => {
if (typeof window !== "undefined") return "";
return process.env.NEXT_PUBLIC_APP_URL!;
})();
return `${base}/api/trpc`;
}
export function TRPCReactProvider(props: {
children: React.ReactNode;
}) {
const queryClient = getQueryClient();
const [trpcClient] = useState(() => {
const httpLinkSingleInstance = httpLink({
url: getUrl(),
transformer: superjson,
fetch(url, options) {
return fetch(url, {
...options,
credentials: "include",
});
},
});
const httpBatchLinkInstance = httpBatchLink({
url: getUrl(),
transformer: superjson,
fetch(url, options) {
return fetch(url, {
...options,
credentials: "include",
});
},
});
const httpSubscriptionLinkInstance = httpSubscriptionLink({
url: getUrl(),
transformer: superjson,
});
const links: TRPCLink<AppRouter>[] = [
loggerLink(),
splitLink({
condition: (op) => op.type === "subscription",
true: httpSubscriptionLinkInstance,
false: splitLink({
condition: (op) => isNonJsonSerializable(op.input),
true: httpLinkSingleInstance,
false: httpBatchLinkInstance,
}),
}),
];
return createTRPCClient<AppRouter>({ links });
});
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{props.children}
</TRPCProvider>
</QueryClientProvider>
);
}
// layout where I am prefetching
import { requireConfiguredAccount } from "@/lib/auth-utils";
import { agentParamsLoader } from "@/modules/agent/server/params-loader";
import { AgentLayout } from "@/modules/agent/ui/layouts/agent-layout";
import { prefetch, trpc } from "@workspace/trpc/server/server";
import type { SearchParams } from "nuqs";
type Props = {
searchParams: Promise<SearchParams>
children: React.ReactNode;
}
const Layout = async ({ children, searchParams }: Props) => {
const { user } = await requireConfiguredAccount();
const params = await agentParamsLoader(searchParams);
prefetch(
trpc.notifications.getUnreadCount.queryOptions()
);
prefetch(
trpc.conversations.getMany.infiniteQueryOptions(params)
);
return (
<AgentLayout image={user.image} name={user.name}>
{children}
</AgentLayout>
)
}
export default Layout;
// auth.ts
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { prisma } from "@workspace/db";
import { sendEmailVerification, sendEmailVerified, sendPasswordReset, sendPasswordResetSuccess } from "../server/modules/internal/mails";
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "postgresql",
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: false,
autoSignIn: true,
sendResetPassword: async ({ user, url }) => {
await sendPasswordReset(user.email, url);
},
onPasswordReset: async ({ user }) => {
await sendPasswordResetSuccess(user.email);
},
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
emailVerification: {
sendOnSignUp: true,
autoSignInAfterVerification: false,
sendVerificationEmail: async ({ user, url }) => {
await sendEmailVerification(user.email, url);
},
onEmailVerification: async (user) => {
await sendEmailVerified(user.email)
},
}
});
now the error
Compiled /api/trpc/[trpc] in 3.7s
GET /api/trpc/notifications.getUnreadCount?batch=1&input=%7B%220%22%3A%7B%22json%22%3Anull%2C%22meta%22%3A%7B%22values%22%3A%5B%22undefined%22%5D%2C%22v%22%3A1%7D%7D%7D 401 in 8238ms
<< query #1 notifications.getUnreadCount {
input: undefined,
result: [Error [TRPCClientError]: You must be logged in to access this resource] {
cause: undefined,
shape: {
message: 'You must be logged in to access this resource',
code: -32001,
data: [Object]
},
data: {
code: 'UNAUTHORIZED',
httpStatus: 401,
stack: 'TRPCError: You must be logged in to access this resource\n' +
' at C:\\Users\\rites\\Desktop\\pathway\\pathway\\apps\\web\\.next\\server\\chunks\\[root-of-the-server]__5b5a3eb5._.js:1244:15\n' +
' at async callRecursive (C:\\Users\\rites\\Desktop\\pathway\\pathway\\apps\\web\\.next\\server\\chunks\\a30f9_@trpc_server_dist_18a3987f._.js:3857:24)\n' +
' at async procedure (C:\\Users\\rites\\Desktop\\pathway\\pathway\\apps\\web\\.next\\server\\chunks\\a30f9_@trpc_server_dist_18a3987f._.js:3882:24)\n' +
' at async C:\\Users\\rites\\Desktop\\pathway\\pathway\\apps\\web\\.next\\server\\chunks\\a30f9_@trpc_server_dist_18a3987f._.js:3189:30\n' +
' at async Promise.all (index 0)\n' +
' at async resolveResponse (C:\\Users\\rites\\Desktop\\pathway\\pathway\\apps\\web\\.next\\server\\chunks\\a30f9_@trpc_server_dist_18a3987f._.js:3403:26)\n' +
' at async fetchRequestHandler (C:\\Users\\rites\\Desktop\\pathway\\pathway\\apps\\web\\.next\\server\\chunks\\a30f9_@trpc_server_dist_18a3987f._.js:3511:12)\n' +
' at async AppRouteRouteModule.do (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\compiled\\next-server\\app-route-turbo.runtime.dev.js:5:38696)\n' +
' at async AppRouteRouteModule.handle (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\compiled\\next-server\\app-route-turbo.runtime.dev.js:5:45978)\n' +
' at async responseGenerator (C:\\Users\\rites\\Desktop\\pathway\\pathway\\apps\\web\\.next\\server\\chunks\\5d657_next_bc54bfd2._.js:10858:38)\n' +
' at async AppRouteRouteModule.handleResponse (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\compiled\\next-server\\app-route-turbo.runtime.dev.js:1:187643)\n' +
' at async handleResponse (C:\\Users\\rites\\Desktop\\pathway\\pathway\\apps\\web\\.next\\server\\chunks\\5d657_next_bc54bfd2._.js:10920:32)\n' +
' at async handler (C:\\Users\\rites\\Desktop\\pathway\\pathway\\apps\\web\\.next\\server\\chunks\\5d657_next_bc54bfd2._.js:10972:13)\n' +
' at async DevServer.renderToResponseWithComponentsImpl (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\base-server.js:1422:9)\n' +
' at async DevServer.renderPageComponent (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\base-server.js:1474:24)\n' +
' at async DevServer.renderToResponseImpl (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\base-server.js:1514:32)\n' +
' at async DevServer.pipeImpl (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\base-server.js:1025:25)\n' +
' at async NextNodeServer.handleCatchallRenderRequest (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\next-server.js:393:17)\n' +
' at async DevServer.handleRequestImpl (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\base-server.js:916:17)\n' +
' at async C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\dev\\next-dev-server.js:399:20\n' +
' at async Span.traceAsyncFn (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\trace\\trace.js:157:20)\n' +
' at async DevServer.handleRequest (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\dev\\next-dev-server.js:395:24)\n' +
' at async invokeRender (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\lib\\router-server.js:240:21)\n' +
' at async handleRequest (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\lib\\router-server.js:437:24)\n' +
' at async requestHandlerImpl (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\lib\\router-server.js:485:13)\n' +
' at async Server.requestListener (C:\\Users\\rites\\Desktop\\pathway\\pathway\\node_modules\\.pnpm\\[email protected]_@opentelemetry+_f94025e8e3d705ec55e3c88f5d76a316\\node_modules\\next\\dist\\server\\lib\\start-server.js:226:13)',
path: 'notifications.getUnreadCount'
},
meta: {
response: Response {
status: 401,
statusText: 'Unauthorized',
headers: Headers {
vary: 'rsc, next-router-state-tree, next-router-prefetch, next-router-segment-prefetch, trpc-accept',
'content-type': 'application/json',
date: 'Tue, 18 Nov 2025 10:22:02 GMT',
connection: 'keep-alive',
'keep-alive': 'timeout=5',
'transfer-encoding': 'chunked'
},
body: ReadableStream { locked: true, state: 'closed', supportsBYOB: true },
bodyUsed: true,
ok: false,
redirected: false,
type: 'basic',
url: 'http://localhost:3000/api/trpc/notifications.getUnreadCount?batch=1&input=%7B%220%22%3A%7B%22json%22%3Anull%2C%22meta%22%3A%7B%22values%22%3A%5B%22undefined%22%5D%2C%22v%22%3A1%7D%7D%7D'
},
responseJSON: [Array]
}
},
elapsedMs: 8367
}
⨯ [Error [TRPCClientError]: You must be logged in to access this resource] {
cause: undefined,
shape: [Object],
data: [Object],
meta: [Object],
digest: '615110479'
}
GET /agent 500 in 24303ms
GET /api/trpc/notifications.getUnreadCount?batch=1&input=%7B%220%22%3A%7B%22json%22%3Anull%2C%22meta%22%3A%7B%22values%22%3A%5B%22undefined%22%5D%2C%22v%22%3A1%7D%7D%7D 200 in 585ms
as you can see at the last line the normal client side query runs and has 200 status code. Please help, I would be grateful. Ask any questions if you have.
I logged the headers when prefetching it logs
{
accept: '*/*',
'accept-encoding': 'gzip, deflate',
'accept-language': '*',
connection: 'keep-alive',
host: 'localhost:3000',
'sec-fetch-mode': 'cors',
'user-agent': 'node',
'x-forwarded-for': '::1',
'x-forwarded-host': 'localhost:3000',
'x-forwarded-port': '3000',
'x-forwarded-proto': 'http'
}
no cookie sent.