BETTER-AUTH. UI

SSR

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

Every Better Auth UI Solid hook uses TanStack Solid Query. A shared QueryClient lets the application prefetch sessions, protect routes, and hydrate the browser cache.

The examples use TanStack Start with Solid Router. They match examples/start-solid-zaidan-example.

Other Solid applications can use the same QueryClient pattern with different router configuration.

Install the SSR integration

npm install @tanstack/solid-query @tanstack/solid-router @tanstack/solid-start solid-js

TanStack Start connects Solid Router to server and browser rendering. Solid Query provides the shared QueryClient.

Pass this client through the route context and AuthProvider. Better Auth UI hooks can then read data that route loaders prefetched.

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/solid-query"
import { createRouter } from "@tanstack/solid-router"

import { routeTree } from "./routeTree.gen"

export const getRouter = () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5000
      }
    }
  })

  return createRouter({
    context: { queryClient },
    defaultPreloadStaleTime: 0,
    routeTree,
    scrollRestoration: true
  })
}

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.
  • Configure defaultOptions as you do in other TanStack Query applications.

Type the root route context

Use createRootRouteWithContext so child routes can read the typed context.queryClient. Then pass this client to the application providers.

src/routes/__root.tsx
import type { QueryClient } from "@tanstack/solid-query"
import { createRootRouteWithContext, Outlet } from "@tanstack/solid-router"

import { Providers } from "@/components/providers"

export const Route = createRootRouteWithContext<{
  queryClient: QueryClient
}>()({
  component: RootComponent
})

function RootComponent() {
  const routeContext = Route.useRouteContext()

  return (
    <Providers queryClient={routeContext().queryClient}>
      <Outlet />
    </Providers>
  )
}

The tree of routes now has { queryClient } available in every loader, beforeLoad, and component via Route.useRouteContext().

Prefetch the session in beforeLoad

Each Solid query provides matching ensure*, prefetch*, and fetch* helpers. Base helpers come from @better-auth-ui/solid or @better-auth-ui/core.

Optional plugin helpers come from @better-auth-ui/core/plugins/<plugin>. These helpers accept QueryClient, authClient, 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.
fetchSessionFetch through the query client, reusing fresh cached data according to Query stale-time rules.

Use ensureSession in beforeLoad to protect a route. Connect the browser and server helpers with createIsomorphicFn.

The server path receives the request headers. The browser path reuses authClient.

src/routes/settings/$path.tsx
import { viewPaths } from "@better-auth-ui/core"
import { ensureSession } from "@better-auth-ui/core"
import { ensureSessionServer } from "@better-auth-ui/core/server"
import { createFileRoute, notFound, redirect } from "@tanstack/solid-router"
import { createIsomorphicFn } from "@tanstack/solid-start"
import { getRequestHeaders } from "@tanstack/solid-start/server"

import { Settings } from "@/components/auth/settings/settings"
import { auth } from "@/lib/auth"
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 ensureSessionIso = createIsomorphicFn()
      .server(() =>
        ensureSessionServer(queryClient, auth, {
          headers: getRequestHeaders()
        })
      )
      .client(() => ensureSession(queryClient, authClient))

    const session = await ensureSessionIso()

    if (!session) {
      throw redirect({
        to: "/auth/$path",
        params: { path: "sign-in" },
        search: { redirectTo: location.href }
      })
    }

    return { session }
  },
  component: SettingsPage
})

function SettingsPage() {
  const path = () => Route.useParams()().path

  return <Settings path={path()} />
}

The loader adds the session to the query cache before the protected route renders. Settings and its useSession calls can read it immediately.

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 session 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 other Solid query helpers. Base helpers such as prefetchListAccounts come from the Solid or core packages.

Plugin helpers come from their core plugin packages. To call the Better Auth server directly, use the core or plugin /server entrypoint.

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/solid-query"
import { createServerFn } from "@tanstack/solid-start"
import { getRequestHeaders } from "@tanstack/solid-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() })

Session server-auth helpers are canonical from @better-auth-ui/core/server as sessionOptionsServer, ensureSessionServer, prefetchSessionServer, and fetchSessionServer.

Available @better-auth-ui/core/server exports include:

  • Session helpers: sessionOptionsServer, ensureSessionServer, prefetchSessionServer, fetchSessionServer
  • Base server auth type: AuthServer
  • Settings helpers: 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 helpers, loader helpers, and component hooks use the same cache keys. Browser hooks can therefore read data that route loaders prefetched.

These helpers do not own secrets, routes, copied interface code, or registry installation. The Solid package also does not create routes.

For TanStack Start installation and component customization, use the Zaidan documentation.

Last updated on

On this page