Aura Auth
Integrations

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.

src/lib/auth.ts
import { createAuth } from "@aura-stack/auth"

export const auth = createAuth({
  oauth: ["github"],
  basePath: "/api/auth",
  baseURL: "http://localhost:3000",
})

export const { handlers, jose, api } = auth

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.

Create an Auth Client Instance

Create an auth-client.ts file in src/lib directory to enable client-side features.

src/lib/auth-client.ts
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.

src/routes/api/auth.$.ts
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

lib/ssr-functions.ts
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.

src/routes/sign-in.tsx
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

src/routes/dashboard.tsx
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.

src/routes/sign-out.tsx
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.

src/components/sign-in-button.tsx
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.

src/components/user-profile.tsx
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.

src/components/sign-out-button.tsx
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 basePath aligned with the route segment. If your route lives at src/routes/api/auth.$.ts, the auth configuration should use basePath: "/api/auth".
  • Use the shared auth module everywhere. Import src/lib/auth.ts from 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.

Resources

On this page