T
TanStack•3mo ago
harsh-harlequin

TanStack Start SSR + Cookie Auth Issue - Need Advice

Hi! I'm facing an SSR authentication issue with TanStack Start and would love some feedback on my solution. The Problem I'm using TanStack Start with cookie-based authentication. Client-side navigation works perfectly, but SSR fails because server-side requests don't include browser cookies. Working: Client navigation (cookies auto-included) Broken: Hard refresh/direct URLs (SSR can't access cookies)
// This fails during SSR
export const Route = createFileRoute("/__root")({
beforeLoad: async ({ context }) => {
// :x: No cookies available here
await context.queryClient.ensureQueryData(meOptions());
},
});
// This fails during SSR
export const Route = createFileRoute("/__root")({
beforeLoad: async ({ context }) => {
// :x: No cookies available here
await context.queryClient.ensureQueryData(meOptions());
},
});
My Proposed Solution Using createIsomorphicFn.
// isomorphic-client.ts
import { createIsomorphicFn } from "@tanstack/start";
import { getHeaders } from "@tanstack/start/server";
import { Api } from "./api-gen"; // swagger-typescript-api

const getServerHeaders = createIsomorphicFn()
.client(() => ({}))
.server(() => {
const headers = getHeaders();
return headers.cookie ? { Cookie: headers.cookie } : {};
});

export const apiClient = createIsomorphicFn()
.client(() => {
return new Api({
baseUrl: import.meta.env.VITE_BASE_API_URL + "/api/user",
baseApiParams: { credentials: "include" },
});
})
.server(() => {
const serverHeaders = getServerHeaders();
return new Api({
baseUrl: process.env.VITE_BASE_API_URL + "/api/user",
baseApiParams: {
credentials: "include",
headers: { ...serverHeaders }, // Forward cookies
},
});
})();
// isomorphic-client.ts
import { createIsomorphicFn } from "@tanstack/start";
import { getHeaders } from "@tanstack/start/server";
import { Api } from "./api-gen"; // swagger-typescript-api

const getServerHeaders = createIsomorphicFn()
.client(() => ({}))
.server(() => {
const headers = getHeaders();
return headers.cookie ? { Cookie: headers.cookie } : {};
});

export const apiClient = createIsomorphicFn()
.client(() => {
return new Api({
baseUrl: import.meta.env.VITE_BASE_API_URL + "/api/user",
baseApiParams: { credentials: "include" },
});
})
.server(() => {
const serverHeaders = getServerHeaders();
return new Api({
baseUrl: process.env.VITE_BASE_API_URL + "/api/user",
baseApiParams: {
credentials: "include",
headers: { ...serverHeaders }, // Forward cookies
},
});
})();
Then just change imports in my fetchers:
// Before
import { apiClient } from "../api/api-client";
// After
import { apiClient } from "../api/isomorphic-client";
// Before
import { apiClient } from "../api/api-client";
// After
import { apiClient } from "../api/isomorphic-client";
Questions 1. Is this the right approach for TanStack Start SSR + cookies? 2. Any gotchas I should be aware of with this solution? 3. Better alternatives? (I need to keep swagger-typescript-api and httpOnly cookies)
7 Replies
other-emerald
other-emerald•3mo ago
I'm using TanStack Start with cookie-based authentication. Client-side navigation works perfectly, but SSR fails because server-side requests don't include browser cookies. Working: Client navigation (cookies auto-included) Broken: Hard refresh/direct URLs (SSR can't access cookies)
isn't it the other way around? Server (server functions, server routes, loaders while on the server, etc. literally everything on the server) can access cookies using getCookie and getCookies. Client cannot access httpOnly cookies Browser initial document request does contain cookies on the same domain as well Is your server and client deployed on different domains? If they are deployed on the same domain, cookies are sent automatically by the browser without us needing to include credentials
harsh-harlequin
harsh-harlequinOP•3mo ago
I'm facing an authentication issue with TanStack Start where cookies aren't being forwarded during SSR. After initial login, everything works perfectly and all API requests include the necessary authentication cookies. However, when I refresh the page, the ensureQueryData calls in my route loaders fail with authentication errors. The browser has the authentication cookies stored (visible in DevTools), but during server-side rendering on refresh, the fetch requests made by TanStack Query don't seem to inherit the cookies from the initial page request. Here's my current setup:
// Route with loader that fails on refresh
export const Route = createFileRoute("/(main)/models")({
loader: async ({ context }) => {
// This works after login but fails on refresh
await context.queryClient.ensureInfiniteQueryData(
productListInfiniteOptions()
);
},
});

// The actual API call that should include cookies
async function getProductList() {
const response = await fetch(`/api/products`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});

if (!response.ok) {
throw new Error('Authentication failed'); // This happens on refresh
}

return response.json();
}
// Route with loader that fails on refresh
export const Route = createFileRoute("/(main)/models")({
loader: async ({ context }) => {
// This works after login but fails on refresh
await context.queryClient.ensureInfiniteQueryData(
productListInfiniteOptions()
);
},
});

// The actual API call that should include cookies
async function getProductList() {
const response = await fetch(`/api/products`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
});

if (!response.ok) {
throw new Error('Authentication failed'); // This happens on refresh
}

return response.json();
}
I understand that server functions and loaders can access cookies using getCookie() and getCookies(), but I'm not sure how to properly forward these cookies to the API calls made within ensureQueryData. Should I be manually extracting cookies from the request context and passing them to my query functions, or is there a more automatic way that TanStack Start handles this cookie forwarding that I might be missing?
other-emerald
other-emerald•3mo ago
Do you have complete reproduction?
Should I be manually extracting cookies from the request context and passing them to my query functions, or is there a more automatic way that TanStack Start handles this cookie forwarding that I might be missing?
"cookie forwarding" is automatic by browser, as long as the cookies are on the same domain. so there shouldn't be any problem here. What I currently do is make all queryFn server functions to access authentication state and deploy the client and server on the same domain so the cookies are automatically sent. I assume you are using cookies for authentication? Your cookie expiration maybe? Or maybe the domain of the cookie you set Regarding the error you are facing, is the log in the browser or your terminal or both?
harsh-harlequin
harsh-harlequinOP•3mo ago
Thank you for the response, I just solved the issue ! The problem was in my API client configuration. I needed to properly forward server headers using TanStack Start's getHeaders(). Here's the solution that worked:
// This is what I added to make it work
export const getCookieHeaders = createServerFn({ method: "GET" }).handler(async () => {
return {
Cookie: getHeaders().cookie || "",
};
});

// Then used it in my API client
export const apiClient = new Api({
securityWorker: async () => ({
credentials: "include",
headers: await getCookieHeaders(), // This forwards server cookies
}),
});
// This is what I added to make it work
export const getCookieHeaders = createServerFn({ method: "GET" }).handler(async () => {
return {
Cookie: getHeaders().cookie || "",
};
});

// Then used it in my API client
export const apiClient = new Api({
securityWorker: async () => ({
credentials: "include",
headers: await getCookieHeaders(), // This forwards server cookies
}),
});
Once I connected this to my API client, all server-side requests in beforeLoad and loader functions started working perfectly with authentication cookies. However, I'm curious why I can't use getHeaders() directly in the securityWorker like this:
// This doesn't work - breaks JS bundle loading
securityWorker: async () => ({
headers: {
Cookie: getHeaders().cookie || "", // Direct usage fails
},
}),
// This doesn't work - breaks JS bundle loading
securityWorker: async () => ({
headers: {
Cookie: getHeaders().cookie || "", // Direct usage fails
},
}),
When I tried the direct approach, cookies were included correctly but the JavaScript bundle wouldn't load properly. I had to wrap getHeaders() in a createServerFn to make everything work. Do you know the technical reason behind this requirement? I'll prepare a reproducible example soon to demonstrate this behavior if you're interested in investigating further.
like-gold
like-gold•3mo ago
apiClient is on the client side right ? then that's why getHeaders is server only
other-emerald
other-emerald•3mo ago
as notKamui said, getHeaders is only accessible on the server side. You probaly used apiClient in the client side Honestly, that is the reason I put all my queryFn in to the server and limited my Api Client to server only. so i can access cookies without worries 😆 . Not sure if that is a good pattern but at least its working
stormy-gold
stormy-gold•7d ago
Thanks for sharing the solutions! I spent whole day figuring out how to solve the same problem.

Did you find this page helpful?