Authentication and session management for TanStack Start applications using WorkOS AuthKit.
Note
This library is designed for TanStack Start v1.0+. TanStack Start is currently in beta - expect some API changes as the framework evolves.
npm install @workos/authkit-tanstack-react-startpnpm add @workos/authkit-tanstack-react-startCreate a .env file in your project root with the following required variables:
WORKOS_CLIENT_ID="client_..." # Get from WorkOS dashboard WORKOS_API_KEY="sk_test_..." # Get from WorkOS dashboard WORKOS_REDIRECT_URI="http://localhost:3000/api/auth/callback" WORKOS_COOKIE_PASSWORD="..." # Min 32 charactersGenerate a secure cookie password (32+ characters):
openssl rand -base64 24| Variable | Default | Description |
|---|---|---|
WORKOS_COOKIE_MAX_AGE | 34560000 (400 days) | Cookie lifetime in seconds |
WORKOS_COOKIE_NAME | wos-session | Session cookie name |
WORKOS_COOKIE_DOMAIN | None | Cookie domain (for multi-domain sessions) |
WORKOS_COOKIE_SAMESITE | lax | SameSite attribute (lax, strict, none) |
WORKOS_API_HOSTNAME | api.workos.com | WorkOS API hostname |
Create or update src/start.ts:
import { createStart } from '@tanstack/react-start'; import { authkitMiddleware } from '@workos/authkit-tanstack-react-start'; export const startInstance = createStart(() => ({ requestMiddleware: [authkitMiddleware()], }));Create src/routes/api/auth/callback.tsx:
import { createFileRoute } from '@tanstack/react-router'; import { handleCallbackRoute } from '@workos/authkit-tanstack-react-start'; export const Route = createFileRoute('/api/auth/callback')({ server: { handlers: { GET: handleCallbackRoute(), }, }, });Make sure this matches your WORKOS_REDIRECT_URI environment variable.
If you want to use useAuth() or other client hooks, wrap your app with AuthKitProvider in src/routes/__root.tsx:
import { AuthKitProvider } from '@workos/authkit-tanstack-react-start/client'; import { Outlet, createRootRoute } from '@tanstack/react-router'; export const Route = createRootRoute({ component: RootComponent, }); function RootComponent() { return ( <AuthKitProvider> <Outlet /> </AuthKitProvider> ); }If you're only using server-side authentication (getAuth() in loaders), you can skip this step.
-
Go to WorkOS Dashboard and navigate to the Redirects page.
-
Under Redirect URIs, add your callback URL:
http://localhost:3000/api/auth/callback -
Under Sign-out redirect, set the URL where you want users to be redirected after signing out. If you don't set a sign-out redirect URL, you must set the App homepage URL instead — WorkOS will redirect users there when no sign-out redirect is specified.
Note: If you don't set either the Sign-out redirect or the App homepage URL, WorkOS will redirect users to an error page.
Use getAuth() in route loaders or server functions to access the current session:
import { createFileRoute, redirect } from '@tanstack/react-router'; import { getAuth, getSignInUrl } from '@workos/authkit-tanstack-react-start'; export const Route = createFileRoute('/dashboard')({ loader: async () => { const { user } = await getAuth(); if (!user) { const signInUrl = await getSignInUrl(); throw redirect({ href: signInUrl }); } return { user }; }, component: DashboardPage, }); function DashboardPage() { const { user } = Route.useLoaderData(); return <div>Welcome, {user.firstName}!</div>; }For client components that need reactive auth state, use the useAuth() hook:
'use client'; // Not actually needed in TanStack Start, but shows intent import { useAuth } from '@workos/authkit-tanstack-react-start/client'; function ProfileButton() { const { user, loading, signOut } = useAuth(); if (loading) return <div>Loading...</div>; if (!user) return <a href="/signin">Sign In</a>; return ( <div> <span>{user.email}</span> <button onClick={() => signOut()}>Sign Out</button> </div> ); }Server-side (in route loader):
import { signOut } from '@workos/authkit-tanstack-react-start'; export const Route = createFileRoute('/logout')({ loader: async () => { await signOut(); // Redirects to WorkOS logout, then back to '/' }, });Client-side (from useAuth hook):
const { signOut } = useAuth(); await signOut({ returnTo: '/goodbye' });Switch the active organization for multi-org users:
Server-side:
import { switchToOrganization } from '@workos/authkit-tanstack-react-start'; // In a server function or loader const auth = await switchToOrganization({ data: { organizationId: 'org_456' }, }); // Session now has org_456's role, permissions, etc.Client-side:
const { switchToOrganization, organizationId } = useAuth(); await switchToOrganization('org_456'); // Auth state updates automaticallyUse layout routes to protect multiple pages:
// src/routes/_authenticated.tsx import { createFileRoute, redirect } from '@tanstack/react-router'; import { getAuth, getSignInUrl } from '@workos/authkit-tanstack-react-start'; export const Route = createFileRoute('/_authenticated')({ loader: async ({ location }) => { const { user } = await getAuth(); if (!user) { const signInUrl = await getSignInUrl({ data: { returnPathname: location.pathname }, }); throw redirect({ href: signInUrl }); } return { user }; }, }); // Now all routes under _authenticated require auth: // - _authenticated/dashboard.tsx // - _authenticated/profile.tsx // etc.These functions can be called from route loaders, server functions, or server route handlers.
Retrieves the current user session.
const { user } = await getAuth(); if (user) { console.log(user.email); console.log(user.firstName); }Returns: UserInfo | NoUserInfo
UserInfo fields:
user- The authenticated user objectsessionId- WorkOS session IDorganizationId- Active organization (if in org context)role- User's role in the organizationroles- Array of role stringspermissions- Array of permission stringsentitlements- Array of entitlement stringsfeatureFlags- Array of feature flag stringsimpersonator- Impersonator details (if being impersonated)accessToken- JWT access token
Signs out the current user and redirects to WorkOS logout.
await signOut(); await signOut({ data: { returnTo: '/goodbye' } });Options:
returnTo- Path to redirect to after logout (default:/)
Switches to a different organization and refreshes the session with new claims.
const auth = await switchToOrganization({ data: { organizationId: 'org_123', returnTo: '/dashboard', // optional }, });Options:
organizationId- The organization ID to switch to (required)returnTo- Path to redirect to if auth fails
Returns: UserInfo with updated organization claims
Generates a sign-in URL for redirecting to AuthKit.
// Basic usage const url = await getSignInUrl(); // With return path const url = await getSignInUrl({ data: { returnPathname: '/dashboard' }, });Options:
returnPathname- Path to return to after sign-in
Generates a sign-up URL for redirecting to AuthKit.
const url = await getSignUpUrl(); const url = await getSignUpUrl({ data: { returnPathname: '/onboarding' }, });Options:
returnPathname- Path to return to after sign-up
Advanced: Generate a custom authorization URL with full control.
const url = await getAuthorizationUrl({ data: { screenHint: 'sign-in', returnPathname: '/dashboard', redirectUri: 'https://example.com/callback', // override default }, });Options:
screenHint-'sign-in'or'sign-up'returnPathname- Return path after authenticationredirectUri- Override the default redirect URI
Handles the OAuth callback from WorkOS. Use this in your callback route.
Basic usage:
import { createFileRoute } from '@tanstack/react-router'; import { handleCallbackRoute } from '@workos/authkit-tanstack-react-start'; export const Route = createFileRoute('/api/auth/callback')({ server: { handlers: { GET: handleCallbackRoute(), }, }, });With hooks for custom logic:
export const Route = createFileRoute('/api/auth/callback')({ server: { handlers: { GET: handleCallbackRoute({ onSuccess: async ({ user, authenticationMethod }) => { // Create user record in your database await db.users.upsert({ id: user.id, email: user.email }); // Track analytics analytics.track('User Signed In', { method: authenticationMethod }); }, onError: ({ error, request }) => { // Custom error handling console.error('Auth failed:', error); return new Response(JSON.stringify({ error: 'Authentication failed' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); }, }), }, }, });Options:
onSuccess?: (data) => Promise<void>- Called after successful authentication with user data, tokens, and authentication methodonError?: ({ error, request }) => Response- Custom error handler that returns a ResponsereturnPathname?: string- Override the redirect path after authentication (defaults to state or/)
Available from @workos/authkit-tanstack-react-start/client. Requires <AuthKitProvider> wrapper.
Access authentication state and methods in client components.
import { useAuth } from '@workos/authkit-tanstack-react-start/client'; function MyComponent() { const { user, loading, signOut } = useAuth(); if (loading) return <div>Loading...</div>; if (!user) return <div>Not signed in</div>; return ( <div> <p>{user.email}</p> <button onClick={() => signOut()}>Sign Out</button> </div> ); }Options:
ensureSignedIn?: boolean- If true, automatically triggers sign-in flow for unauthenticated users
Returns: AuthContextType with:
user- Current user or nullloading- Loading statesessionId,organizationId,role,roles,permissions,entitlements,featureFlags,impersonatorgetAuth()- Refresh auth staterefreshAuth(options)- Refresh session with optional org switchsignOut(options)- Sign outswitchToOrganization(orgId)- Switch organizations
Manage access tokens with automatic refresh.
import { useAccessToken } from '@workos/authkit-tanstack-react-start/client'; function ApiCaller() { const { accessToken, loading, getAccessToken } = useAccessToken(); const callApi = async () => { const token = await getAccessToken(); // Always fresh const response = await fetch('/api/data', { headers: { Authorization: `Bearer ${token}` }, }); }; return <button onClick={callApi}>Fetch Data</button>; }Returns:
accessToken- Current token (may be stale)loading- Loading stateerror- Last error or nullrefresh()- Manually refresh tokengetAccessToken()- Get guaranteed fresh token
Parse and decode JWT claims from the access token.
import { useTokenClaims } from '@workos/authkit-tanstack-react-start/client'; function ClaimsDisplay() { const claims = useTokenClaims(); if (!claims) return null; return ( <div> <p>Session ID: {claims.sid}</p> <p>Organization: {claims.org_id}</p> <p>Role: {claims.role}</p> </div> ); }Processes authentication on every request. Validates tokens, refreshes sessions, and provides auth context to server functions.
import { authkitMiddleware } from '@workos/authkit-tanstack-react-start'; // Basic usage authkitMiddleware(); // With custom redirect URI (e.g., for Vercel preview deployments) authkitMiddleware({ redirectUri: 'https://preview-123.example.com/api/auth/callback', });Options:
redirectUri- Override the default redirect URI fromWORKOS_REDIRECT_URI. Useful for dynamic environments like preview deployments.
This library is fully typed. Common types:
import type { User, Session, UserInfo, NoUserInfo, Impersonator } from '@workos/authkit-tanstack-react-start'; // User object from WorkOS const user: User = { id: string; email: string; firstName: string | null; lastName: string | null; emailVerified: boolean; profilePictureUrl: string | null; // ... more fields }; // Auth result from getAuth() const auth: UserInfo | NoUserInfo = await getAuth();Route loaders get full type inference:
export const Route = createFileRoute('/profile')({ loader: async () => { const { user } = await getAuth(); return { user }; // Fully typed }, component: ProfilePage, }); function ProfilePage() { const { user } = Route.useLoaderData(); // user is typed! }- Middleware runs on every request - validates/refreshes session, stores auth in context
- Route loaders call
getAuth()- retrieves auth from middleware context - No client bundle bloat - server functions create RPC boundaries automatically
- Provider wraps app - provides auth context to hooks
- Hooks call server actions - fetch auth state via RPC
- State updates automatically - on tab focus, refresh, org switch
- Server-only apps: Just use
getAuth()in loaders - no provider needed - Client hooks needed: Add provider to use
useAuth(),useAccessToken(), etc. - Flexibility: Start server-only, add client hooks later
// Get sign-in URL in loader export const Route = createFileRoute('/')({ loader: async () => { const { user } = await getAuth(); const signInUrl = await getSignInUrl(); return { user, signInUrl }; }, component: HomePage, }); function HomePage() { const { user, signInUrl } = Route.useLoaderData(); if (!user) { return <a href={signInUrl}>Sign In with AuthKit</a>; } return <div>Welcome, {user.firstName}!</div>; }// src/routes/_authenticated.tsx import { createFileRoute, redirect } from '@tanstack/react-router'; import { getAuth, getSignInUrl } from '@workos/authkit-tanstack-react-start'; export const Route = createFileRoute('/_authenticated')({ loader: async ({ location }) => { const { user } = await getAuth(); if (!user) { const signInUrl = await getSignInUrl({ data: { returnPathname: location.pathname }, }); throw redirect({ href: signInUrl }); } return { user }; }, }); // All child routes require authentication: // - _authenticated/dashboard.tsx // - _authenticated/settings.tsximport { useAuth } from '@workos/authkit-tanstack-react-start/client'; function OrgSwitcher() { const { organizationId, switchToOrganization } = useAuth(); return ( <select value={organizationId || ''} onChange={(e) => switchToOrganization(e.target.value)} > <option value="org_123">Acme Corp</option> <option value="org_456">Other Company</option> </select> ); }Loader (server-side):
loader: async () => { const { user, organizationId, role } = await getAuth(); return { user, organizationId, role }; };Component (from loader data):
function MyPage() { const { user } = Route.useLoaderData(); // ... }Client hook (reactive):
function MyClientComponent() { const { user, loading } = useAuth(); // Updates on session changes }You forgot to add authkitMiddleware() to src/start.ts. See step 1 in setup.
You're calling useAuth() but haven't wrapped your app with <AuthKitProvider>. See step 3 in setup.
If you don't need client hooks, use getAuth() in loaders instead.
The middleware validates configuration on first request. If you see errors about missing variables:
- Check your
.envfile exists - Verify all required variables are set
- Ensure
WORKOS_COOKIE_PASSWORDis 32+ characters - Restart your dev server after changing env vars
Make sure you're importing from the right path:
// Server functions import { getAuth, signOut } from '@workos/authkit-tanstack-react-start'; // Client hooks import { useAuth } from '@workos/authkit-tanstack-react-start/client';Don't import client hooks in server code or vice versa.
You're trying to call a server function from a beforeLoad hook or client component.
Wrong:
beforeLoad: async () => { const { user } = await getAuth(); // ❌ Runs on client during hydration };Right:
loader: async () => { const { user } = await getAuth(); // ✅ Server-only during SSR };Use useAuth() client hook for client components, or move logic to a loader.
Check the /example directory for a complete working application demonstrating:
- Server-side authentication in loaders
- Client-side hooks with provider
- Protected routes
- Organization switching
- Sign in/out flows
- Access token management
Run it locally:
cd example pnpm install pnpm dev- TanStack Start: v1.132.0+
- TanStack Router: v1.132.0+
- React: 18.0+
- Node.js: 18+
MIT