SSR
Customize the TanStack Query client and prefetch auth data on the server with TanStack Start.
Every Better Auth UI hook uses TanStack Query. A shared QueryClient lets the application prefetch sessions, protect routes, and hydrate the browser cache.
The examples use TanStack Start and match examples/start-heroui-example. Other React applications can use the same QueryClient pattern with different router configuration.
Install the SSR integration
npm install @tanstack/react-query @tanstack/react-router-ssr-query@tanstack/react-router-ssr-query dehydrates the QueryClient on the server. It sends that state with the HTML and rehydrates it in the browser.
The package also wraps the application in QueryClientProvider. You do not need another provider.
Customize the QueryClient
Create the QueryClient inside the router factory. Each SSR request then receives a separate cache.
Apply defaultOptions in this factory. The example uses a staleTime of 5 seconds to reuse recent results during navigation.
import { QueryClient } from "@tanstack/react-query"
import { createRouter } from "@tanstack/react-router"
import { setupRouterSsrQueryIntegration } from "@tanstack/react-router-ssr-query"
import { routeTree } from "./routeTree.gen"
export const getRouter = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5000
}
}
})
const router = createRouter({
routeTree,
scrollRestoration: true,
defaultPreloadStaleTime: 0,
context: { queryClient }
})
setupRouterSsrQueryIntegration({
router,
queryClient
})
return router
}The options have these effects:
defaultPreloadStaleTime: 0tells TanStack Router to start loaders during each preload. QuerystaleTimecan still reuse cached data.context: { queryClient }exposes the client to every loader/beforeLoadhook viacontext.queryClient.- Call
setupRouterSsrQueryIntegrationaftercreateRouter. It uses router events to dehydrate and hydrate data during navigation.
Configure defaultOptions as you do in other TanStack Query applications. For example, disable refetch on window focus with this option:
new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000,
refetchOnWindowFocus: false
}
}
})Type the root route context
Use createRootRouteWithContext so child routes can read context.queryClient with full typing.
import type { QueryClient } from "@tanstack/react-query"
import { createRootRouteWithContext } from "@tanstack/react-router"
export const Route = createRootRouteWithContext<{
queryClient: QueryClient
}>()({
// ...head, shellComponent, etc.
})The tree of routes now has { queryClient } available in every loader, beforeLoad, and component via Route.useRouteContext().
Prefetch the session in beforeLoad
Each @better-auth-ui/react query provides matching ensure*, prefetch*, and fetch* helpers. These helpers accept authClient, QueryClient, and the query parameters.
| Helper | When to use |
|---|---|
ensureSession | Read the session, resolving from cache if fresh. Most common in loaders. |
prefetchSession | Kick off a background fetch without awaiting. Good for soft preloads. |
fetchSession | Always bypass the cache and fetch fresh data. |
Use ensureSession in beforeLoad to protect a route. If it returns no session, redirect the user to sign-in.
Preserve the current URL in the redirect. The application can return the user to this URL after sign-in.
import { viewPaths } from "@better-auth-ui/core"
import { Settings } from "@better-auth-ui/heroui"
import { ensureSession } from "@better-auth-ui/core"
import { createFileRoute, notFound, redirect } from "@tanstack/react-router"
import { authClient } from "@/lib/auth-client"
export const Route = createFileRoute("/settings/$path")({
async beforeLoad({ params: { path }, context: { queryClient }, location }) {
if (!Object.values(viewPaths.settings).includes(path)) {
throw notFound()
}
const session = await ensureSession(queryClient, authClient)
if (!session) {
throw redirect({
to: "/auth/$path",
params: { path: "sign-in" },
search: { redirectTo: location.href }
})
}
return { user: session.user }
},
component: SettingsPage
})
function SettingsPage() {
const { path } = Route.useParams()
const { user } = Route.useRouteContext()
return (
<div className="w-full max-w-3xl mx-auto p-4 md:p-6">
<Settings path={path} />
</div>
)
}The server adds the session to the query cache. As a result, Settings and its useSession calls read it during the first render.
TanStack Router adds the value from beforeLoad to the route context. Read this value in a component with Route.useRouteContext().
This pattern avoids another query for the same user data.
Prefetch without blocking
Use prefetchSession to fill the cache without blocking navigation. This pattern can prepare a public route that later shows an authenticated widget:
import { prefetchSession } from "@better-auth-ui/core"
export const Route = createFileRoute("/")({
loader: ({ context: { queryClient } }) => {
void prefetchSession(queryClient, authClient)
}
})The same pattern works for each settings query. The related prefetch helpers and options factories are available from @better-auth-ui/core.
Server-only helpers
To avoid an HTTP request, import the session helpers from @better-auth-ui/core/server. Call them from a server function or another server runtime.
These helpers accept the Better Auth server instance instead of authClient:
import { ensureSessionServer } from "@better-auth-ui/core/server"
import type { QueryClient } from "@tanstack/react-query"
import { createServerFn } from "@tanstack/react-start"
import { getRequestHeaders } from "@tanstack/react-start/server"
import { auth } from "@/lib/auth"
const getSession = createServerFn().handler(() =>
auth.api.getSession({ headers: getRequestHeaders() })
)
export const ensureServerSession = (queryClient: QueryClient) =>
ensureSessionServer(queryClient, auth, { headers: getRequestHeaders() })@better-auth-ui/core/server exports sessionOptionsServer, ensureSessionServer, prefetchSessionServer, and fetchSessionServer. They accept the Better Auth server instance and request parameters.
Available @better-auth-ui/core/server helper families include:
- Session:
sessionOptionsServer,ensureSessionServer,prefetchSessionServer,fetchSessionServer - Settings:
listAccountsOptions,ensureListAccounts,prefetchListAccounts,fetchListAccounts,accountInfoOptions,ensureAccountInfo,listSessionsOptions, andensureListSessionswith matching prefetch/fetch helpers
Plugin server query helper families live under their plugin server entrypoints:
- API-key (
@better-auth-ui/core/plugins/api-key/server):listApiKeysOptions,ensureListApiKeys,prefetchListApiKeys,fetchListApiKeys - Multi-session (
@better-auth-ui/core/plugins/multi-session/server):listDeviceSessionsOptions,ensureListDeviceSessions,prefetchListDeviceSessions,fetchListDeviceSessions - Passkey (
@better-auth-ui/core/plugins/passkey/server):listPasskeysOptions,ensureListPasskeys,prefetchListPasskeys,fetchListPasskeys - Organization (
@better-auth-ui/core/plugins/organization/server):activeOrganizationOptions,ensureActiveOrganization,prefetchActiveOrganization,fetchActiveOrganization,fullOrganizationOptions,ensureFullOrganization,listOrganizationsOptions,ensureListOrganizations,listOrganizationMembersOptions,ensureListOrganizationMembers,listOrganizationInvitationsOptions,ensureListOrganizationInvitations,listUserInvitationsOptions,ensureListUserInvitations,hasPermissionOptions, andensureHasPermissionwith matching prefetch/fetch helpers
Server and client helpers use the same cache keys. Therefore, browser hooks such as useSession can read data that the server prefetched.
Last updated on