BETTER-AUTH. UI

SSR

Customize the TanStack Query client and prefetch auth data on the server with TanStack Start.

Every Better Auth UI hook runs through TanStack Query, so wiring up a shared QueryClient and its router integration is all you need to prefetch sessions on the server, guard routes before render, and hydrate the cache on the client.

The examples below mirror examples/start-heroui-example and target TanStack Start. The same QueryClient recipe works in any React app — only the router integration differs.

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, streams it down with the HTML, and rehydrates it on the client. It also wraps the app in a QueryClientProvider for you, so no extra provider is required.

Customize the QueryClient

Build the QueryClient inside your router factory so every SSR request gets a fresh cache. Apply your defaultOptions here — the example sets a 5 second staleTime so repeat navigations within that window skip the network.

src/router.tsx
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
}

A few notes on the options above:

  • defaultPreloadStaleTime: 0 tells TanStack Router to always run loaders on preload. Pair it with staleTime on the query so the underlying data is still cached — the loader fires, the query returns instantly.
  • context: { queryClient } exposes the client to every loader/beforeLoad hook via context.queryClient.
  • setupRouterSsrQueryIntegration must be called after createRouter — it subscribes to router events to dehydrate/hydrate around each navigation.

Tune defaultOptions the same way you would in any TanStack Query app. For example, to disable refetch-on-window-focus globally:

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.

src/routes/__root.tsx
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

Every @better-auth-ui/react query ships matching ensure*, prefetch*, and fetch* helpers alongside its options factory. They accept the authClient, a QueryClient, and the same params as the underlying query.

HelperWhen to use
ensureSessionRead the session, resolving from cache if fresh. Most common in loaders.
prefetchSessionKick off a background fetch without awaiting. Good for soft preloads.
fetchSessionAlways bypass the cache and fetch fresh data.

Use ensureSession in beforeLoad to gate protected routes. If no session is returned, redirect to sign-in and preserve the current URL so the user lands back after signing in.

src/routes/settings/$path.tsx
import { viewPaths } from "@better-auth-ui/core"
import { Settings } from "@better-auth-ui/heroui"
import { ensureSession } from "@better-auth-ui/react"
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>
  )
}

Because the query is seeded on the server, the Settings component — and any useSession call inside it — reads the session synchronously on first render, with no loading flash.

Anything you return from beforeLoad is merged into the route context, so you can forward the resolved user (or any other derived data) straight to the component via Route.useRouteContext() instead of re-reading the query downstream.

Prefetch without blocking

Use prefetchSession when you want to warm the cache without blocking navigation — for example, on a marketing route that lazily renders an authed widget:

import { prefetchSession } from "@better-auth-ui/react"

export const Route = createFileRoute("/")({
  loader: ({ context: { queryClient } }) => {
    void prefetchSession(queryClient, authClient)
  }
})

The same pattern works for every settings read — prefetchListAccounts, prefetchListSessions, prefetchListDeviceSessions, prefetchListPasskeys, and prefetchAccountInfo all live in @better-auth-ui/react next to their options factories.

Server-only helpers

If you'd rather skip the HTTP round-trip and hit the Better Auth server directly from a TanStack Start server function (or any other server runtime), import the matching helpers from @better-auth-ui/react/server. They accept your Better Auth server instance instead of an authClient:

src/lib/session.ts
import { ensureSession } from "@better-auth-ui/react/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) =>
  ensureSession(queryClient, auth, { headers: getRequestHeaders() })

The server entrypoint mirrors supported read helpers with server-auth signatures: sessionOptions, ensureSession, prefetchSession, fetchSession, plus equivalents for settings, passkey, API-key, and Organization queries. These helpers accept your Better Auth server instance (auth) and request params instead of a browser authClient.

Available server query helper families include:

  • Session: sessionOptions, ensureSession, prefetchSession, fetchSession
  • Settings/passkey: listAccountsOptions, ensureListAccounts, prefetchListAccounts, fetchListAccounts, accountInfoOptions, ensureAccountInfo, listSessionsOptions, ensureListSessions, listDeviceSessionsOptions, ensureListDeviceSessions, listPasskeysOptions, and ensureListPasskeys with matching prefetch/fetch helpers
  • API-key: listApiKeysOptions, ensureListApiKeys, prefetchListApiKeys, fetchListApiKeys
  • Organization: activeOrganizationOptions, ensureActiveOrganization, prefetchActiveOrganization, fetchActiveOrganization, fullOrganizationOptions, ensureFullOrganization, listOrganizationsOptions, ensureListOrganizations, listOrganizationMembersOptions, ensureListOrganizationMembers, listOrganizationInvitationsOptions, ensureListOrganizationInvitations, listUserInvitationsOptions, ensureListUserInvitations, hasPermissionOptions, and ensureHasPermission with matching prefetch/fetch helpers

Cache keys are identical between server and client helpers, so data prefetched on the server hydrates cleanly into client-side hooks like useSession, useListApiKeys, and useActiveOrganization.

Last updated on

On this page