TLDR: When using TanStack Start with authentication handled on a different subdomain (e.g., api.domain.com), fetching the user session directly in your SSR loader won’t work as expected because server-to-server requests don’t include browser cookies. To fix the header flicker and SSR the correct user state, create a server function that forwards the original request’s cookies to your auth server. Call this function in your loader to ensure the authenticated user is available during SSR, eliminating the flicker and improving UX.
The situation is the following, you're developing an app with Tanstack Start, you deploy it to app.domain.com
, but you have a separate API server that handles logic and authentication using better-auth
, which runs on api.subdomain.com
.
You create a header in your app that displays the logged in user, but you see that when you log in and reload the app, the header flickers because initially it displays the logged out state and once the user data arrives, it flips to the logged in state.
Of course you're a good developer who wants to deliver good UX, so you decide to fix it by fetching the user on the server and then the proper logged in/out state will get rendered into the HTML and the flicker is gone.
What you do is you go and write a loader
function like this:
export const Route = createRootRouteWithContext<{
queryClient: QueryClient;
}>()({
loader: async () => {
const session = await authClient.getSession();
return { user: session?.data?.user }
},
component: RootComponent,
});
and then in the RootComponent
, you have access to user
and can pass it to your Header
or Navigation
component, whatever you want to call it.
function RootComponent() {
const { user } = Route.useRouteContext();
return (
<html>
<head>
<HeadContent />
</head>
<body>
<AuthProvider>
<Navigation user={user} />
<main className="min-h-screen bg-background">
<Outlet />
</main>
<TanStackRouterDevtools />
<Toaster />
<Scripts />
</AuthProvider>
</body>
</html>
);
}
Great, it was very easy, now you go and open your app to marvel at your excellence and then boom, the flicker is still there. You scratch your head, you have no idea what's going wrong (I know I didn't).
This is where if this article existed, it could have saved me quite a few agonizing hours, hope it can for someone else as well.
The problem is that when authClient.getSession
makes a request to api.domain.com
, this request is going from your server that is running tanstack start to your server that is running auth, which means there are no cookies involved here, since cookies are stored in the browser. This is where you might say, but hey, the browser is making the request to the tanstack server to render the page, so that request must have the cookies, right? If you said this, you're absolutely right, this is what we're going to take advantage of to solve the problem.
The solution is, you have to create a server function that fetches the user and then call that server function in your loader. I don't think this is documented anywhere (or maybe I haven't found it), but server function include all credentials like cookies from the original request the browser made, so you can write one like this:
export const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
const event = getEvent();
const session = await authClient.getSession({
fetchOptions: {
headers: {
Cookie: event.headers.get("Cookie") || "", // For brevity I copy all cookies here, but you might want to just parse out the auth cookie and send that.
},
},
});
if (!session.data) {
return null;
}
return session.data.user;
});
call it in your loader:
loader: async () => {
const user = await fetchUser();
return { user };
},
and you're done. With this you can now get the authenticated user on the server and properly SSR your pages.
Top comments (0)