router Documentation

A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).

14,892 stars TypeScript TypeScript 1 file · ~12,517 tokens #framework#fullstack#javascript#react#route#router#routing#rpc
RAW Doc

File: README.md

| ## TanStack Router

A modern router designed for type safety, data‑driven navigation, and seamless developer experience.

  • End‑to-end type safety (routes, params, loaders)
  • Schema‑driven search params with validation
  • Built‑in caching, prefetching & invalidation
  • Nested layouts, transitions & error boundaries

[Read the Router Docs →](https://tanstack.com/router) | ## TanStack Start

A full‑stack framework built on Router, designed for server rendering, streaming, and production‑ready deployments.

  • Full‑document SSR & streaming
  • Server functions & end‑to‑end type safety
  • Deployment‑ready bundling & builds
  • All the power of TanStack Router, plus full‑stack features

[Read the Start Docs →](https://tanstack.com/start) |

| --- | --- |

Get Involved

Partners

Explore the TanStack Ecosystem

… and more at TanStack.com »


File: benchmarks/ssr/README.md

SSR Benchmarks

Cross-framework SSR request-loop benchmarks for:

  • @tanstack/react-start
  • @tanstack/solid-start
  • @tanstack/vue-start

Each benchmark builds a Start app with file-based routes and runs Vitest benches against the built server handler.

Layout

  • react/ - React Start baseline benchmark + Vitest config
  • solid/ - Solid Start baseline benchmark + Vitest config
  • vue/ - Vue Start baseline benchmark + Vitest config
  • vitest.react.config.ts, vitest.solid.config.ts, vitest.vue.config.ts - per-framework aggregate configs that run the baseline first, then scenario projects
  • scenarios/<scenario>/<framework>/ - isolated scenario apps

Scenario app layout:

text
scenarios/<scenario>/<framework>/
  vite.config.ts
  speed.bench.ts
  tsconfig.json
  src/
    router.tsx
    routes/
    routeTree.gen.ts

Each scenario uses one app per framework instead of sharing routes in the baseline app. This keeps route-tree size, middleware, Start options, and generated route trees isolated so one scenario cannot shift another scenario's numbers. The existing baseline apps and bench names stay stable for CodSpeed continuity.

Scenario Responsibilities

Each scenario isolates one Start server-side responsibility so benchmark changes can be attributed to a specific feature area.

Scenario Start server-side responsibility
react/, solid/, vue/ baseline apps Document SSR for nested file routes, route matching, search parsing, and full-page HTML response generation.
assets Per-request asset pipeline work: CSS inlining, CDN asset URL transforms with uncached manifest resolution, and response Link header collection for early hints.
before-load Nested beforeLoad execution: sequential per-match context building, context merging across matches, and context consumption by loaders during document SSR.
control-flow Loader-thrown redirect, notFound, plain errors, unmatched routes, and route headers() emission, including HTTP status selection, redirect location headers, route error boundaries, and not-found HTML rendering.
global-middleware Global createStart middleware registration: request middleware wrapping document SSR, server-function, and server-route requests, plus global function middleware on server functions.
head Nested route head evaluation, title/meta/link serialization, and head-entry deduplication during SSR.
loaders Nested route loader execution, loader deps from search params, router context reads, and dehydrated loader payload generation.
rewrites Composed location rewrites from router basepath plus locale input/output rewrites; unprefixed URLs redirect to /app/* when the basepath output rewrite canonicalizes them.
serialization Dehydration and RPC serialization of rich non-plain-JSON types: seroval native types in loader payloads, custom serialization adapters, and shallow-error serialization.
selective-ssr Route-level ssr modes: rendered server HTML with ssr: true, dehydrated data without HTML with ssr: 'data-only', and client-only omission with ssr: false.
server-fn-transport Non-JSON server-function transport: multipart/FormData request decoding, raw Response returns, and RawStream streamed responses through the binary frame protocol.
server-fns createServerFn GET/POST request handling, thrown redirect/notFound results, sendContext serialization, SSR-time direct calls, middleware context, input validation, and URL discovery.
server-routes File route server.handlers dispatch for parameterized JSON API routes without document HTML rendering.
server-routes-middleware Server route request middleware chains, middleware-provided context merging, and handler access to accumulated context.
streaming Deferred loader data, Await/Suspense fallback output, larger streamed HTML body scanning, streamed HTML ordering, and later dehydration payload emission.

Run

Run all benchmarks through Nx so dependency builds are part of the graph:

bash
pnpm nx run @benchmarks/ssr:test:perf --outputStyle=stream --skipRemoteCache

Run framework-specific benchmarks:

bash
pnpm nx run @benchmarks/ssr:test:perf:react --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:test:perf:solid --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:test:perf:vue --outputStyle=stream --skipRemoteCache

Build framework-specific benchmark apps:

bash
pnpm nx run @benchmarks/ssr:build:react --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:build:solid --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr:build:vue --outputStyle=stream --skipRemoteCache

Typecheck benchmark sources:

bash
pnpm nx run @benchmarks/ssr:test:types --outputStyle=stream --skipRemoteCache

Run one scenario app manually through Nx:

bash
pnpm nx run @benchmarks/ssr-<scenario>-<framework>:build:ssr --outputStyle=stream --skipRemoteCache
pnpm nx run @benchmarks/ssr-<scenario>-<framework>:test:types:ssr --outputStyle=stream --skipRemoteCache

Use react, solid, or vue for <framework>. The baseline projects use @benchmarks/ssr-<framework> without a scenario segment.

Request Conventions

  • Document GET loops use accept: text/html, matching the baseline request shape.
  • Server-function loops must include sec-fetch-site: same-origin so the default CSRF middleware accepts the request.
  • Loops that expect non-200 responses pass a custom validateResponse to runRequestLoop.
  • Bench loops must build deterministic requests from the seeded random helper and consume response bodies through runRequestLoop or runSsrRequestLoop.

File: benchmarks/client-nav/README.md

Client Navigation Benchmarks

Cross-framework client-side CPU benchmarks for:

  • @tanstack/react-router
  • @tanstack/solid-router
  • @tanstack/vue-router

The benchmarks run in jsdom against production builds of real apps, and are
tracked in CI by CodSpeed (simulation mode).

Scope: these benchmarks cover the standalone client router only. The
client side of TanStack Start (hydration of a server-rendered document,
streamed payload consumption, server-function calls from the client, ...) is
not covered here; the server side of Start is covered by benchmarks/ssr.

Layout

  • react/, solid/, vue/ - baseline benchmark (mixed navigation loop) + Vitest config
  • vitest.react.config.ts, vitest.solid.config.ts, vitest.vue.config.ts - per-framework aggregate configs that run the baseline first, then scenario projects
  • scenarios/harness.ts - shared scenario runner (mount, link-click steps, onRendered synchronization)
  • scenarios/<scenario>/shared.ts - framework-agnostic scenario definition (workload data, step sequence, assertions, bench options)
  • scenarios/<scenario>/<framework>/ - isolated scenario apps

Scenario app layout:

text
scenarios/<scenario>/<framework>/
  vite.config.ts
  speed.bench.ts
  speed.flame.ts
  setup.ts
  project.json
  tsconfig.json
  src/
    main.tsx
    routeTree.gen.ts
    routes/

Scenario apps use file-based routing (@tanstack/router-plugin) with a
generated routeTree.gen.ts, like a regular user app. Each scenario uses one
app per framework instead of sharing routes in the baseline app. This keeps
route-tree size and router options isolated so one scenario cannot shift
another scenario's numbers. The existing baseline apps and bench names stay
stable for CodSpeed continuity.

Scenario Responsibilities

Each scenario isolates one client-side responsibility so benchmark changes can
be attributed to a specific feature area.

Scenario Client-side responsibility
react/, solid/, vue/ baseline apps Mixed navigation loop: path params, search params, route context, and useParams/useSearch/useLoaderData selector subscriptions.
async-pipeline The router's async pipeline via counted 0ms timer hops: async loaders (transition-held navigation), async beforeLoad context, and parallel nested async loaders. Component-level Await/Suspense is excluded: React 19 throttles Suspense reveals by ~300ms wall-clock, which is inherently non-deterministic to benchmark.
control-flow Loader-thrown redirect (including a 2-hop chain), notFound() with notFoundComponent, loader errors with errorComponent, and boundary reset on recovery navigation.
head HeadContent per-navigation work: nested route head() evaluation, title/meta/link dedupe across matches, and head tag DOM updates during navigation.
history History push/replace/back/forward traversal, location masking, registered-but-never-blocking useBlocker, and useCanGoBack/useLocation subscriptions.
links Per-navigation cost of ~200 mounted <Link>s: link prop building, active-state recompute across activeOptions variants, activeProps swaps, and useMatchRoute probes (the MatchRoute component is avoided: vue-router's implementation leaks one subscription per render).
loaders Client loader dispatch: always-stale re-runs (staleTime: 0), cached revisits (re-run once per lap by the invalidate step), loaderDeps-keyed caching, router.invalidate(), and useLoaderData selectors.
mount Cold start: createRouter (route-tree processing) + first render + initial router.load() + unmount, with a fresh router per mount. The only scenario measuring router creation.
nested-params Deep nesting (8 dynamic levels): per-level params.parse/stringify, beforeLoad context accumulation across matches, and per-level useParams/useRouteContext subscriptions. Param values include characters requiring percent-encoding (as do route-tree-scale's), so segment encode/decode paths run on every navigation.
preload Intent preloading from hover events, programmatic router.preloadRoute, deterministic preload cache behavior (defaultPreloadStaleTime: 0), and commit-time cache maintenance.
rewrites Composed client-side location rewrites: router basepath plus a locale input/output rewrite pair, running on every href build and location parse (the client analog of the SSR rewrites scenario).
route-tree-scale Route matching and link-target resolution on a wide (~40 route) tree mixing static, dynamic, prefixed-param, splat, pathless-layout, and route-group paths, with autoCodeSplitting enabled so navigations also resolve lazy route chunks.
search-params validateSearch execution, search middlewares (retainSearchParams/stripSearchParams), functional search updaters, structural sharing, and useSearch selector subscriptions.

Conventions

  • Apps are built with NODE_ENV=production (minify: false) into dist/app.js; benches import the built bundle, so production package builds and production JSX output are measured, not dev transforms.
  • Scenarios behave like a real user app: navigation happens through <Link> clicks dispatched on real anchor elements (unless a scenario specifically measures the imperative API), the router uses the default browser history, and scrollRestoration is enabled.
  • Each benchmark iteration advances a fixed, circular sequence of steps; every step awaits the router's onRendered event, so render work is included and steps cannot overlap. No two consecutive steps may target the same location, and the sequence ends back on the initial route.
  • Setup runs one warm-up lap through the sequence and asserts each step's observable output (e.g. document.title), so a scenario that silently stops doing its work fails instead of reporting a fast time.
  • Determinism: no wall-clock timers (async work is resolved promises or counted setTimeout(0) hops), staleTime/gcTime only ever 0 or effectively-infinite, no Math.random/Date.now in the measured loop. Scroll restoration uses getScrollRestorationKey: (location) => location.pathname so its cache stays bounded (the default random per-entry key grows one cache entry per push navigation). The mount scenario opts out of scrollRestoration entirely and destroys its history on unmount — both register page-lifetime globals that would leak across its mount/unmount loop.
  • Push-only step laps grow jsdom's session-history entry list over a run (jsdom never truncates entries behind the current index). Per-push cost stays O(1) so timings are unaffected; only the history scenario needs — and has — a lap that is depth-stationary.
  • Calibration: pick per-iteration step counts so a bench's vitest bench run stays roughly between 8 and 30 seconds (long enough to average out, short enough for CI).

Run

Run all benchmarks through Nx so dependency builds are part of the graph:

bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf --outputStyle=stream --skipRemoteCache

Run framework-specific benchmarks (baseline + all scenarios):

bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:react --outputStyle=stream --skipRemoteCache
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:solid --outputStyle=stream --skipRemoteCache
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:perf:vue --outputStyle=stream --skipRemoteCache

Run a single scenario app manually (after building it through Nx):

bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav-<scenario>-<framework>:build:client --outputStyle=stream --skipRemoteCache
cd benchmarks/client-nav && NODE_ENV=production vitest bench --config ./scenarios/<scenario>/<framework>/vite.config.ts

Run framework-specific flame benchmarks (10 second loop, profiled with @platformatic/flame, forced to NODE_ENV=production):

bash
# baseline
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:react --outputStyle=stream --skipRemoteCache
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:solid --outputStyle=stream --skipRemoteCache
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:flame:vue --outputStyle=stream --skipRemoteCache
# scenarios
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav-<scenario>-<framework>:test:flame --outputStyle=stream --skipRemoteCache

Typecheck benchmark sources (baseline + scenarios):

bash
CI=1 NX_DAEMON=false pnpm nx run @benchmarks/client-nav:test:types --outputStyle=stream --skipRemoteCache

File: benchmarks/bundle-size/README.md

Bundle Size Benchmarks

This workspace contains deterministic bundle-size fixtures for:

  • @tanstack/react-router
  • @tanstack/solid-router
  • @tanstack/vue-router
  • @tanstack/react-start
  • @tanstack/solid-start

Each package has minimal and full scenarios:

  • minimal: Small route app with __root + index route that renders hello world
  • full: Same route shape plus a broad root-level harness that imports/uses the full hooks/components surface
  • Start full scenarios also exercise createServerFn, createMiddleware, and useServerFn
  • Start deferred-hydration scenarios match the minimal route shape and wrap the index route content in Hydrate

Design Notes

  • Scenarios use file-based routing as the default app style.
  • Router scenarios use @tanstack/router-plugin/vite with autoCodeSplitting: true.
  • Start Vite scenarios use @tanstack/<framework>-start/plugin/vite with router code-splitting enabled.
  • React Start also includes Rsbuild scenarios using @tanstack/react-start/plugin/rsbuild.
  • Full-surface coverage is manually maintained (no strict export-coverage gate).
  • Primary metrics measure all emitted client JS chunks and are reported as raw/gzip/brotli bytes.
  • Initial-load JS graph metrics are also recorded as initialRawBytes, initialGzipBytes, and initialBrotliBytes for context.
  • Gzip for all emitted client JS is the primary tracking signal for PR deltas and historical charting.

Local Run

bash
pnpm nx run @benchmarks/bundle-size:build

Run one or more scenarios during local optimization:

bash
pnpm nx run @benchmarks/bundle-size:build -- --scenario react-router.minimal
pnpm nx run @benchmarks/bundle-size:build -- --scenario react-router.minimal,react-router.full

Filtered runs build only the package projects needed by selected scenarios. Full runs build all package projects needed by all scenarios. If the required packages are already built and unchanged, skip that step:

bash
pnpm nx run @benchmarks/bundle-size:build -- --scenario react-router.minimal --skip-package-builds

This writes:

  • benchmarks/bundle-size/results/current.json
  • benchmarks/bundle-size/results/benchmark-action.json

current.json includes run status, selected package build projects, per-scenario totals, per-file sizes, and the emitted JS files used for measurement. Dist paths use scenarioDir/outDir, e.g. react-router.minimal maps to benchmarks/bundle-size/dist/react-router-minimal/.

Local Query Tools

bash
pnpm benchmark:bundle-size:query --id react-router.minimal
pnpm benchmark:bundle-size:diff --baseline /tmp/base-current.json --id react-router.minimal
pnpm benchmark:bundle-size:history --id react-router.minimal --top-deltas 20

For source attribution, run an analysis build. This uses hidden source maps and writes source estimates into current.json; those estimates are for investigation only, not tracking.

bash
pnpm nx run @benchmarks/bundle-size:build -- --scenario react-router.minimal --analysis
pnpm benchmark:bundle-size:analyze --id react-router.minimal --top-sources 30

CI Reporting

  • PR workflow generates a sticky comment with:
    • current gzip values
    • baseline delta
    • inline sparkline trend
  • Pushes to main publish historical chart data to GitHub Pages via benchmark-action/github-action-benchmark.

Manual Update Policy

When router/start public hooks/components evolve, update the corresponding *-full/src/routes/__root.tsx harness to keep full scenarios representative.

Backfill Readiness

The measurement script supports optional interfaces for historical backfilling:

  • --sha
  • --measured-at
  • --append-history
  • --scenario
  • --analysis
  • --sourcemap
  • --skip-package-builds

These are intended for one-off scripts that replay historical commits and append results to the same history dataset shape used for chart generation.
If --append-history points at a data.js file, output is written as window.BENCHMARK_DATA = ... for direct GitHub Pages compatibility.


File: docs/start/eslint/eslint-plugin-start.md


id: eslint-plugin-start title: ESLint Plugin Start

TanStack Start comes with its own ESLint plugin. This plugin is used to enforce best practices and help you avoid common mistakes when working with Server Components and server/client boundaries.

Installation

The plugin is a separate package that you need to install:

sh
npm install -D @tanstack/eslint-plugin-start

or

sh
pnpm add -D @tanstack/eslint-plugin-start

or

sh
yarn add -D @tanstack/eslint-plugin-start

or

sh
bun add -D @tanstack/eslint-plugin-start

Flat Config (`eslint.config.js`)

The release of ESLint 9.0 introduced a new way to configure ESLint using a flat config format. The TanStack Start ESLint Plugin supports this format and provides a recommended config you can use to enable all recommended rules.

To enable all recommended rules:

js
// eslint.config.js
import pluginStart from '@tanstack/eslint-plugin-start'

export default [
  pluginStart.configs['flat/recommended'],
  // Any other config...
]

Custom Flat Config setup

Alternatively, load the plugin and configure only the rules you want:

js
// eslint.config.js
import pluginStart from '@tanstack/eslint-plugin-start'

export default [
  {
    plugins: {
      '@tanstack/start': pluginStart,
    },
    rules: {
      '@tanstack/start/no-client-code-in-server-component': 'error',
      '@tanstack/start/no-async-client-component': 'error',
    },
  },
  // Any other config...
]

Legacy Config (`.eslintrc`)

Prior to ESLint 9.0, the most common way to configure ESLint was using a .eslintrc file. The TanStack Start ESLint plugin still supports this configuration method.

To enable all recommended rules, add plugin:@tanstack/eslint-plugin-start/recommended in extends:

json
{
  "extends": ["plugin:@tanstack/eslint-plugin-start/recommended"]
}

Custom Legacy Config setup

Alternatively, add @tanstack/eslint-plugin-start to plugins and configure the rules you want:

json
{
  "plugins": ["@tanstack/eslint-plugin-start"],
  "rules": {
    "@tanstack/start/no-client-code-in-server-component": "error",
    "@tanstack/start/no-async-client-component": "error"
  }
}

Rules

The following rules are available in the TanStack Start ESLint plugin:


File: docs/start/eslint/no-async-client-component.md


id: no-async-client-component title: Disallow async components in client context

Async React components are only valid within React Server Components.

In TanStack Start, that means async components must only be used in server-component context, for example:

  • rendered by renderServerComponent(<AsyncComp />)
  • rendered inside a createCompositeComponent((props) => <AsyncComp />) callback

This rule helps you catch async components that have become "client" (directly or transitively), which would otherwise crash at runtime.

Rule Details

Examples of incorrect code for this rule:

tsx
/* eslint "@tanstack/start/no-async-client-component": "error" */

import { createFileRoute } from '@tanstack/react-router'

export async function Page() {
  return 
}

export const Route = createFileRoute(undefined)({
  component: Page,
})

Examples of correct code for this rule:

tsx
/* eslint "@tanstack/start/no-async-client-component": "error" */

import { createFileRoute } from '@tanstack/react-router'

export function Page() {
  return 
}

export const Route = createFileRoute(undefined)({
  component: Page,
})
tsx
/* eslint "@tanstack/start/no-async-client-component": "error" */

import { createCompositeComponent } from '@tanstack/react-start/rsc'

export const ServerPage = createCompositeComponent(async () => {
  const message = await Promise.resolve('hello')
  return {message}
})

Reporting location

This rule can report in two ways:

  • Usage-site: if a component becomes client due to being referenced by route options, the error should be reported at the route option usage.
  • Definition-site: if a component is client due to 'use client' or being rendered by a client component, the error is reported at the component definition.

Attributes

  • ✅ Recommended

File: docs/start/eslint/no-client-code-in-server-component.md


id: no-client-code-in-server-component title: Disallow client-only code in server components

TanStack Start defines React Server Component boundaries via RSC helpers like renderServerComponent() and createCompositeComponent().

  • renderServerComponent(<Element />) is a server boundary around the element.
  • createCompositeComponent((props) => <Element />) is a server boundary around the callback.

Inside these boundaries (and anything rendered from them, unless it crosses a 'use client' boundary) you must not use client-only code.

This rule detects common client-only patterns in server components:

  • React client hooks (eg useState, useEffect)
  • browser-only globals (eg window, document)
  • React event handlers (onClick, etc.)
  • function props (non-serializable across the server/client boundary)
  • class components

Rule Details

Examples of incorrect code for this rule:

tsx
/* eslint "@tanstack/start/no-client-code-in-server-component": "error" */

import { createCompositeComponent } from '@tanstack/react-start/rsc'
import { useState } from 'react'

export const Server = createCompositeComponent(() => {
  const [count] = useState(0)
  return {count}
})
tsx
/* eslint "@tanstack/start/no-client-code-in-server-component": "error" */

import { createCompositeComponent } from '@tanstack/react-start/rsc'

export const Server = createCompositeComponent(() => {
  return {window.location.href}
})

Examples of correct code for this rule:

tsx
/* eslint "@tanstack/start/no-client-code-in-server-component": "error" */

import { renderServerComponent } from '@tanstack/react-start/rsc'

export const Server = renderServerComponent(
  (async () => {
    const msg = await Promise.resolve('ok')
    return {msg}
  })(),
)

Attributes

  • ✅ Recommended

File: docs/start/framework/solid/guide/authentication.md


id: authentication title: Authentication

This guide covers authentication patterns and shows how to implement your own authentication system with TanStack Start.

📋 Before You Start: Check our Authentication Overview for all available options including partner solutions and hosted services.

Authentication Approaches

You have several options for authentication in your TanStack Start application:

Hosted Solutions:

  1. Clerk - Complete authentication platform with UI components
  2. WorkOS - Enterprise-focused with SSO and compliance features
  3. Better Auth - Open-source TypeScript library
  4. Auth.js - Open-source library supporting 80+ OAuth providers

DIY Implementation Benefits:

  • Full Control: Complete customization over authentication flow
  • No Vendor Lock-in: Own your authentication logic and user data
  • Custom Requirements: Implement specific business logic or compliance needs
  • Cost Control: No per-user pricing or usage limits

Authentication involves many considerations including password security, session management, rate limiting, CSRF protection, and various attack vectors.

Core Concepts

Authentication vs Authorization

  • Authentication: Who is this user? (Login/logout)
  • Authorization: What can this user do? (Permissions/roles)

TanStack Start provides the tools for both through server functions, sessions, and route protection.

Protect the data/API boundary first. Any server function, server route, or other API endpoint that returns or mutates private data must authorize the request itself. beforeLoad is useful for route UX: it keeps users out of screens they cannot use and avoids triggering work that would fail anyway. It is not the security boundary for the data. See Authentication Server Primitives for the server-side pattern.

Essential Building Blocks

1. Server Functions for Authentication

Server functions handle sensitive authentication logic securely on the server:

tsx
import { createServerFn } from '@tanstack/solid-start'
import { redirect } from '@tanstack/solid-router'

// Login server function
export const loginFn = createServerFn({ method: 'POST' })
  .validator((data: { email: string; password: string }) => data)
  .handler(async ({ data }) => {
    // Verify credentials (replace with your auth logic)
    const user = await authenticateUser(data.email, data.password)

    if (!user) {
      return { error: 'Invalid credentials' }
    }

    // Create session
    const session = await useAppSession()
    await session.update({
      userId: user.id,
      email: user.email,
    })

    // Redirect to protected area
    throw redirect({ to: '/dashboard' })
  })

// Logout server function
export const logoutFn = createServerFn({ method: 'POST' }).handler(async () => {
  const session = await useAppSession()
  await session.clear()
  throw redirect({ to: '/' })
})

// Get current user
export const getCurrentUserFn = createServerFn({ method: 'GET' }).handler(
  async () => {
    const session = await useAppSession()
    const userId = session.get('userId')

    if (!userId) {
      return null
    }

    return await getUserById(userId)
  },
)

2. Session Management

TanStack Start provides secure HTTP-only cookie sessions:

tsx
// utils/session.ts
import { useSession } from '@tanstack/solid-start/server'

type SessionData = {
  userId?: string
  email?: string
  role?: string
}

export function useAppSession() {
  return useSession<SessionData>({
    // Session configuration
    name: 'app-session',
    password: process.env.SESSION_SECRET!, // At least 32 characters
    // Optional: customize cookie settings
    cookie: {
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      httpOnly: true,
    },
  })
}

3. Authentication Context

Share authentication state across your application:

tsx
// contexts/auth.tsx
import { createContext, useContext } from 'solid-js'
import { useServerFn } from '@tanstack/solid-start'
import { getCurrentUserFn } from '../server/auth'

type User = {
  id: string
  email: string
  role: string
}

type AuthContextType = {
  user: User | null
  isLoading: boolean
  refetch: () => void
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export function AuthProvider(props) {
  const { data: user, isLoading, refetch } = useServerFn(getCurrentUserFn)

  return (
    <AuthContext.Provider value={{ user, isLoading, refetch }}>
      {props.children}
    </AuthContext.Provider>
  )
}

export function useAuth() {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within AuthProvider')
  }
  return context
}

4. Route Protection

Protect routes using beforeLoad:

tsx
// routes/_authed.tsx - Layout route for protected pages
import { createFileRoute, redirect } from '@tanstack/solid-router'
import { getCurrentUserFn } from '../server/auth'

export const Route = createFileRoute('/_authed')({
  beforeLoad: async ({ location }) => {
    const user = await getCurrentUserFn()

    if (!user) {
      throw redirect({
        to: '/login',
        search: { redirect: location.href },
      })
    }

    // Pass user to child routes
    return { user }
  },
})
tsx
// routes/_authed/dashboard.tsx - Protected route
import { createFileRoute } from '@tanstack/solid-router'

export const Route = createFileRoute('/_authed/dashboard')({
  component: DashboardComponent,
})

function DashboardComponent() {
  const context = Route.useRouteContext()

  return (
    
      <h1>Welcome, {context().user.email}!</h1>
      {/* Dashboard content */}
    
  )
}

Implementation Patterns

Basic Email/Password Authentication

tsx
// server/auth.ts
import bcrypt from 'bcryptjs'
import { createServerFn } from '@tanstack/solid-start'

// User registration
export const registerFn = createServerFn({ method: 'POST' })
  .validator((data: { email: string; password: string; name: string }) => data)
  .handler(async ({ data }) => {
    // Check if user exists
    const existingUser = await getUserByEmail(data.email)
    if (existingUser) {
      return { error: 'User already exists' }
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(data.password, 12)

    // Create user
    const user = await createUser({
      email: data.email,
      password: hashedPassword,
      name: data.name,
    })

    // Create session
    const session = await useAppSession()
    await session.update({ userId: user.id })

    return { success: true, user: { id: user.id, email: user.email } }
  })

async function authenticateUser(email: string, password: string) {
  const user = await getUserByEmail(email)
  if (!user) return null

  const isValid = await bcrypt.compare(password, user.password)
  return isValid ? user : null
}

Role-Based Access Control (RBAC)

tsx
// utils/auth.ts
export const roles = {
  USER: 'user',
  ADMIN: 'admin',
  MODERATOR: 'moderator',
} as const

type Role = (typeof roles)[keyof typeof roles]

export function hasPermission(userRole: Role, requiredRole: Role): boolean {
  const hierarchy = {
    [roles.USER]: 0,
    [roles.MODERATOR]: 1,
    [roles.ADMIN]: 2,
  }

  return hierarchy[userRole] >= hierarchy[requiredRole]
}

// Protected route with role check
export const Route = createFileRoute('/_authed/admin/')({
  beforeLoad: async ({ context }) => {
    if (!hasPermission(context.user.role, roles.ADMIN)) {
      throw redirect({ to: '/unauthorized' })
    }
  },
})

Social Authentication Integration

tsx
// Example with OAuth providers
export const authProviders = {
  google: {
    clientId: process.env.GOOGLE_CLIENT_ID!,
    redirectUri: `${process.env.APP_URL}/auth/google/callback`,
  },
  github: {
    clientId: process.env.GITHUB_CLIENT_ID!,
    redirectUri: `${process.env.APP_URL}/auth/github/callback`,
  },
}

export const initiateOAuthFn = createServerFn({ method: 'POST' })
  .validator((data: { provider: 'google' | 'github' }) => data)
  .handler(async ({ data }) => {
    const provider = authProviders[data.provider]
    const state = generateRandomState()

    // Store state in session for CSRF protection
    const session = await useAppSession()
    await session.update({ oauthState: state })

    // Generate OAuth URL
    const authUrl = generateOAuthUrl(provider, state)

    throw redirect({ href: authUrl })
  })

Password Reset Flow

tsx
// Password reset request
export const requestPasswordResetFn = createServerFn({ method: 'POST' })
  .validator((data: { email: string }) => data)
  .handler(async ({ data }) => {
    const user = await getUserByEmail(data.email)
    if (!user) {
      // Don't reveal if email exists
      return { success: true }
    }

    const token = generateSecureToken()
    const expires = new Date(Date.now() + 60 * 60 * 1000) // 1 hour

    await savePasswordResetToken(user.id, token, expires)
    await sendPasswordResetEmail(user.email, token)

    return { success: true }
  })

// Password reset confirmation
export const resetPasswordFn = createServerFn({ method: 'POST' })
  .validator((data: { token: string; newPassword: string }) => data)
  .handler(async ({ data }) => {
    const resetToken = await getPasswordResetToken(data.token)

    if (!resetToken || resetToken.expires < new Date()) {
      return { error: 'Invalid or expired token' }
    }

    const hashedPassword = await bcrypt.hash(data.newPassword, 12)
    await updateUserPassword(resetToken.userId, hashedPassword)
    await deletePasswordResetToken(data.token)

    return { success: true }
  })

Security Best Practices

1. Password Security

tsx
// Use strong hashing (bcrypt, scrypt, or argon2)
import bcrypt from 'bcryptjs'

const saltRounds = 12 // Adjust based on your security needs
const hashedPassword = await bcrypt.hash(password, saltRounds)

2. Session Security

tsx
// Use secure session configuration
export function useAppSession() {
  return useSession({
    name: 'app-session',
    password: process.env.SESSION_SECRET!, // 32+ characters
    cookie: {
      secure: process.env.NODE_ENV === 'production', // HTTPS only in production
      sameSite: 'lax', // CSRF protection
      httpOnly: true, // XSS protection
      maxAge: 7 * 24 * 60 * 60, // 7 days
    },
  })
}

3. Rate Limiting

tsx
// Simple in-memory rate limiting (use Redis in production)
const loginAttempts = new Map<string, { count: number; resetTime: number }>()

export const rateLimitLogin = (ip: string): boolean => {
  const now = Date.now()
  const attempts = loginAttempts.get(ip)

  if (!attempts || now > attempts.resetTime) {
    loginAttempts.set(ip, { count: 1, resetTime: now + 15 * 60 * 1000 }) // 15 min
    return true
  }

  if (attempts.count >= 5) {
    return false // Too many attempts
  }

  attempts.count++
  return true
}

4. Input Validation

tsx
import { z } from 'zod'

const loginSchema = z.object({
  email: z.string().email().max(255),
  password: z.string().min(8).max(100),
})

export const loginFn = createServerFn({ method: 'POST' })
  .validator((data) => loginSchema.parse(data))
  .handler(async ({ data }) => {
    // data is now validated
  })

Testing Authentication

Unit Testing Server Functions

tsx
// __tests__/auth.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { loginFn } from '../server/auth'

describe('Authentication', () => {
  beforeEach(async () => {
    await setupTestDatabase()
  })

  it('should login with valid credentials', async () => {
    const result = await loginFn({
      data: { email: '[email protected]', password: 'password123' },
    })

    expect(result.error).toBeUndefined()
    expect(result.user).toBeDefined()
  })

  it('should reject invalid credentials', async () => {
    const result = await loginFn({
      data: { email: '[email protected]', password: 'wrongpassword' },
    })

    expect(result.error).toBe('Invalid credentials')
  })
})

Integration Testing

tsx
// __tests__/auth-flow.test.tsx
import { render, screen, fireEvent, waitFor } from '@solidjs/testing-library'
import { RouterProvider, createMemoryHistory } from '@tanstack/solid-router'
import { router } from '../router'

describe('Authentication Flow', () => {
  it('should redirect to login when accessing protected route', async () => {
    const history = createMemoryHistory()
    history.push('/dashboard') // Protected route

    render(<RouterProvider router={router} history={history} />)

    await waitFor(() => {
      expect(screen.getByText('Login')).toBeInTheDocument()
    })
  })
})

Common Patterns

Loading States

tsx
function LoginForm() {
  const [isLoading, setIsLoading] = createSignal(false)
  const loginMutation = useServerFn(loginFn)

  const handleSubmit = async (data: LoginData) => {
    setIsLoading(true)
    try {
      await loginMutation.mutate(data)
    } catch (error) {
      // Handle error
    } finally {
      setIsLoading(false)
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* Form fields */}
      
        {isLoading() ? 'Logging in...' : 'Login'}
      
    </form>
  )
}

Remember Me Functionality

tsx
export const loginFn = createServerFn({ method: 'POST' })
  .validator(
    (data: { email: string; password: string; rememberMe?: boolean }) => data,
  )
  .handler(async ({ data }) => {
    const user = await authenticateUser(data.email, data.password)
    if (!user) return { error: 'Invalid credentials' }

    const session = await useAppSession()
    await session.update(
      { userId: user.id },
      {
        // Extend session if remember me is checked
        maxAge: data.rememberMe ? 30 * 24 * 60 * 60 : undefined, // 30 days vs session
      },
    )

    return { success: true }
  })

Migration from Other Solutions

From Client-Side Auth

If you're migrating from client-side authentication (localStorage, context only):

  1. Move authentication logic to server functions
  2. Replace localStorage with server sessions
  3. Update route protection to use beforeLoad
  4. Add proper security headers and CSRF protection

From Other Frameworks

  • Next.js: Replace API routes with server functions, migrate NextAuth sessions
  • Remix: Convert loaders/actions to server functions, adapt session patterns
  • SvelteKit: Move form actions to server functions, update route protection

Production Considerations

When choosing your authentication approach, consider these factors:

Hosted vs DIY Comparison

Hosted Solutions (Clerk, WorkOS, Better Auth):

  • Pre-built security measures and regular updates
  • UI components and user management features
  • Compliance certifications and audit trails
  • Support and documentation
  • Per-user or subscription pricing

DIY Implementation:

  • Complete control over implementation and data
  • No ongoing subscription costs
  • Custom business logic and workflows
  • Responsibility for security updates and monitoring
  • Need to handle edge cases and attack vectors

Security Considerations

Authentication systems need to handle various security aspects:

  • Password hashing and timing attack prevention
  • Session management and fixation protection
  • CSRF and XSS protection
  • Rate limiting and brute force prevention
  • OAuth flow security
  • Compliance requirements (GDPR, CCPA, etc.)

Next Steps

When implementing authentication, consider:

  • Security Review: Review your implementation for security best practices
  • Performance: Add caching for user lookups and session validation
  • Monitoring: Add logging and monitoring for authentication events
  • Compliance: Ensure compliance with relevant regulations if storing personal data

For other authentication approaches, check the Authentication Overview. For specific integration help, see the How-to Guides or explore our working examples.


File: docs/start/framework/solid/guide/authentication-overview.md


ref: docs/start/framework/react/guide/authentication-overview.md
replace:
{
'@tanstack/react-start': '@tanstack/solid-start',
'React': 'SolidJS',
'react-router': 'solid-router',

}


File: docs/start/framework/solid/guide/client-entry-point.md


id: client-entry-point title: Client Entry Point

Client Entry Point

[!NOTE]
The client entry point is optional out of the box. If not provided, TanStack Start will automatically handle the client entry point for you using the below as a default.

Getting our html to the client is only half the battle. Once there, we need to hydrate our client-side JavaScript once the route resolves to the client. We do this by hydrating the root of our application with the StartClient component:

tsx
// src/client.tsx
import { hydrate } from 'solid-js/web'
import { StartClient, hydrateStart } from '@tanstack/solid-start/client'

hydrateStart().then((router) => {
  hydrate(() => <StartClient router={router} />, document)
})

This enables us to kick off client-side routing once the user's initial server request has fulfilled.

Error Handling

You can wrap your client entry point with error boundaries to handle client-side errors gracefully:

tsx
// src/client.tsx
import { StartClient } from '@tanstack/solid-start/client'
import { hydrate } from 'solid-js/web'
import { ErrorBoundary } from './components/ErrorBoundary'

hydrate(
  () => (
    <ErrorBoundary>
      <StartClient />
    </ErrorBoundary>
  ),
  document.body,
)

Development vs Production

You may want different behavior in development vs production:

tsx
// src/client.tsx
import { StartClient } from '@tanstack/solid-start/client'
import { hydrate } from 'solid-js/web'

const App = (
  <>
    {import.meta.env.DEV && Development Mode}
    <StartClient />
  </>
)

hydrate(() => <App />, document.body)

The client entry point gives you full control over how your application initializes on the client side while working seamlessly with TanStack Start's server-side rendering.