TanStack Start
Integrate Aura Auth and React TanStack Start
This guide walks you through to implement Aura Auth in a React TanStack Start application to a complete support for Server Functions and API Routes. If you haven't configured Aura Auth yet, start with the Installation Guide and Quick Start Guide to set up your Auth instance and environment variables. Then follow the steps in this guide to integrate Aura Auth with your React TanStack Start application.
Aura Auth is working on developing a React TanStack Start-specific package that will provide additional utilities and hooks for React TanStack Start applications. For now, the core package can be used to implement authentication in React TanStack Start with the patterns outlined in this guide. Stay tuned for updates on the React TanStack Start package!
Setup Aura Auth
Create an Auth Instance
Create an auth.ts file in src/lib directory to configure your Aura Auth instance.
import { createAuth } from "@aura-stack/auth"
export const auth = createAuth({
oauth: ["github"],
basePath: "/api/auth",
baseURL: "http://localhost:3000",
})
export const { handlers, jose, api } = authThe basePath should match the path where your auth route handlers are mounted and baseURL should point to your local
development server or deployed application URL.
Create an Auth Client Instance
Create an auth-client.ts file in src/lib directory to enable client-side features.
import { createAuthClient } from "@aura-stack/auth/client"
export const authClient = createAuthClient({
basePath: "/api/auth",
baseURL: "http://localhost:3000",
})The basePath should match the path where your auth route handlers are mounted and baseURL should point to your local
development server or deployed application URL. Both options should be the same as the ones used in your auth.ts configuration
to ensure client and server instances are aligned.
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.