Aura Auth
Integrations

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:

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.ts server configuration
  • an API route at src/routes/api/auth.$.ts that forwards requests to Aura Auth handlers
  • a set of shared server functions in src/lib/ssr-functions.ts for sign-in, sign-out, and session lookup
  • an optional src/lib/auth-client.ts browser client
  • server and client examples for authentication flows

Project Structure

auth.$.ts
auth.ts
auth-client.ts
ssr-functions.ts
.env.local

Environment Setup

Create a .env.local file at the root of your project to store secrets securely.

.env.local
# 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"
Never commit your .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.

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

basePath 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.

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

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