TanStack Start
Build your first authentication flow with Aura Auth and TanStack Start
This guide walks you through creating a complete authentication flow using Aura Auth in a TanStack Start application.
Overview
TanStack Start server functions and API routes natively utilize the Web Standard Request and Response interfaces by wrapping the underlying Vinxi/Nitro engine. That makes integrating Aura Auth straightforward: you can mount the shared handlers in one route and use server functions to manage auth state.
Before continuing, complete the installation and initial setup:
- Quick Start Guide to create your Aura Auth instance
- TypeScript Configuration for TypeScript-specific setup
- TanStack Start Integration App for a fully working example
Then use this guide to integrate Aura Auth with a TanStack Start server using best practices.
What You'll Build
You will create a small TanStack Start setup with:
- a shared
src/lib/auth.tsserver configuration - an API route at
src/routes/api/auth.$.tsthat forwards requests to Aura Auth handlers - a set of shared server functions in
src/lib/ssr-functions.tsfor sign-in, sign-out, and session lookup - an optional
src/lib/auth-client.tsbrowser client - server and client examples for authentication flows
Project Structure
Environment Setup
Create a .env.local file at the root of your project to store secrets securely.
# 32-bytes (256-bit) secret used to sign/encrypt sessions. Use a secure random value.
AURA_AUTH_SECRET="base64-or-hex-32-bytes"
AURA_AUTH_SALT="base64-or-hex-32-bytes".env.local file to version control. Use a secret manager in production.Setup Aura Auth
HTTP Handlers
Create your auth.ts instance inside src/lib/ to configure authentication and export the helpers used by API routes and server functions.
import { createAuth } from "@aura-stack/auth"
export const auth = createAuth({
oauth: ["github"],
basePath: "/api/auth",
baseURL: "http://localhost:3000",
})
export const { handlers, jose, api } = authbasePath must match the route mounted in src/routes/api/auth.$.ts. baseURL should point to your local development server or deployed application URL.
Client API
To use Aura Auth's client-side features, create an auth-client.ts file to initialize the client.
import { createAuthClient } from "@aura-stack/auth/client"
export const authClient = createAuthClient({
basePath: "/api/auth",
baseURL: "http://localhost:3000",
})The baseURL should point to your server's URL, and basePath MUST match the path where your auth routes are mounted.
When you deploy, keep the same basePath but update baseURL to your production domain or environment-specific public URL.
Mount HTTP Handlers
Create a catch-all route that forwards all auth requests to Aura Auth handlers. TanStack Start passes standard Web Request objects, so you can delegate directly to the shared handler map.
import { handlers } from "@/lib/auth"
import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/api/auth/$")({
server: {
handlers: {
GET: async ({ request }) => {
return await handlers.GET(request)
},
POST: async ({ request }) => {
return await handlers.POST(request)
},
PATCH: async ({ request }) => {
return await handlers.PATCH(request)
},
},
},
})This route handles all requests under /api/auth/*.
Server Side Rendering (SSR)
Use server functions and loaders when you want auth to happen before the page renders. This is the best place to protect pages and manage redirect-driven flows.
Server Functions
import { api } from "@/lib/auth"
import { redirect } from "@tanstack/react-router"
import { createServerFn } from "@tanstack/react-start"
import { getRequest, getRequestHeaders } from "@tanstack/react-start/server"
export const getSession = createServerFn({ method: "GET" }).handler(async () => {
try {
const result = await api.getSession({
headers: getRequestHeaders(),
})
if (!result.authenticated) return null
return result.session
} catch (error) {
console.error("[error:server] getSession", error)
return null
}
})
export const signOutFn = createServerFn({ method: "POST" }).handler(async () => {
try {
const response = await api.signOut({
headers: getRequestHeaders(),
})
throw redirect({ to: "/", headers: response.headers, reloadDocument: true })
} catch (error) {
console.error("[error:server] signOut", error)
throw redirect({ to: "/", reloadDocument: true })
}
})
export const signInFn = createServerFn({ method: "POST" })
.inputValidator((data: { provider: string }) => {
if (!data || typeof data.provider !== "string" || !data.provider.trim()) {
throw new Error("provider must be a non-empty string")
}
return data
})
.handler(async ({ data }) => {
try {
const response = await api.signIn(data.provider, {
request: getRequest(),
redirect: false,
})
throw redirect({
href: response.signInURL,
})
} catch (error) {
console.error("[error:server] signIn", error)
throw redirect({ to: "/login" })
}
})Sign In
This pattern lets a form submission create the sign-in request while TanStack Start handles the page transition.
import { signInFn } from "@/lib/ssr-functions"
import { useServerFn } from "@tanstack/react-start"
export function SignInPage() {
const signIn = useServerFn(signInFn)
const handleSignIn = async (provider: string) => {
await signIn({ data: { provider } })
}
return (
<div>
<h1>Login</h1>
<button onClick={() => handleSignIn("github")}>Sign in with GitHub</button>
</div>
)
}Get Session
import { createFileRoute, redirect } from "@tanstack/react-router"
import { getSession } from "@/lib/ssr-functions"
export const Route = createFileRoute("/dashboard")({
loader: async () => {
const session = await getSession()
if (!session) {
throw redirect({ to: "/login" })
}
return session
},
component: DashboardPage,
})
export function DashboardPage() {
const session = Route.useLoaderData()
return <h1>Welcome, {session.user?.name}!</h1>
}This is the right pattern for protected pages: resolve the session in the loader, redirect if needed, and only render private UI when the session is present.
Sign Out
Use the server function when sign-out should happen from a dedicated page or route-level mutation.
import { signOutFn } from "@/lib/ssr-functions"
import { useServerFn } from "@tanstack/react-start"
export function SignOutPage() {
const signOut = useServerFn(signOutFn)
const handleSignOut = async () => {
await signOut()
}
return (
<div>
<h1>Sign Out</h1>
<button onClick={handleSignOut}>Sign Out</button>
</div>
)
}Client Side Rendering (CSR)
Use client components for interactive UI such as buttons, menus, and dialogs. The browser client is useful when you do not need a full route transition.
Sign In
If you want a redirect-less flow, set redirect: false and use the returned URL yourself.
import { authClient } from "@/lib/auth-client"
export const SignInButton = () => {
const handleSignIn = async () => {
await authClient.signIn("github", {
redirectTo: "/dashboard",
})
}
return <button onClick={handleSignIn}>Sign in with GitHub</button>
}Get Session
This works well for avatars, nav bars, and any component that should update after hydration.
import { useEffect, useState } from "react"
import { authClient } from "@/lib/auth-client"
export const UserProfile = () => {
const [session, setSession] = useState(null)
useEffect(() => {
const fetchSession = async () => {
const result = await authClient.getSession()
setSession(result.session ?? null)
}
fetchSession()
}, [])
if (!session) return <p>Loading...</p>
return (
<div>
<h1>Welcome back, {session.user?.name}</h1>
<p>Email: {session.user?.email}</p>
</div>
)
}Sign Out
Client-side sign-out is best for interactive controls inside menus or profile popovers.
import { authClient } from "@/lib/auth-client"
export const SignOutButton = () => {
const handleSignOut = async () => {
await authClient.signOut({
redirect: true,
redirectTo: "/",
})
}
return <button onClick={handleSignOut}>Sign Out</button>
}Common Pitfalls
- Keep
basePathaligned with the route segment. If your route lives atsrc/routes/api/auth.$.ts, the auth configuration should usebasePath: "/api/auth". - Use the shared auth module everywhere. Import
src/lib/auth.tsfrom the API route and server functions so there is one auth configuration. - Protect pages in loaders when possible. If a page should never render for unauthenticated users, check the session before passing it to the component.
- Use the browser client for interaction, not page protection. Client components are great for buttons and menus, but server-side checks are still the safer default for private routes.