BETTER-AUTH. UI

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.

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
}

The options have these effects:

  • defaultPreloadStaleTime: 0 tells TanStack Router to start loaders during each preload. Query staleTime can still reuse cached data.
  • context: { queryClient } exposes the client to every loader/beforeLoad hook via context.queryClient.
  • Call setupRouterSsrQueryIntegration after createRouter. 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.

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

Each @better-auth-ui/react query provides matching ensure*, prefetch*, and fetch* helpers. These helpers accept authClient, QueryClient, and the query parameters.

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 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.

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/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:

src/lib/session.ts
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, and ensureListSessions with 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, and ensureHasPermission with 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

On this page