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,createCsrfMiddleware}from'@tanstack/react-start';import{authkitMiddleware}from'@workos/authkit-tanstack-react-start';// Reject cross-site requests to server-function RPC endpoints.constcsrfMiddleware=createCsrfMiddleware({filter: (ctx)=>ctx.handlerType==='serverFn',});exportconststartInstance=createStart(()=>({requestMiddleware: [csrfMiddleware,authkitMiddleware()],}));Why
createCsrfMiddleware? TanStack Start applies CSRF protection to server functions automatically — but only when your app doesn't define its ownstartInstance. RegisteringauthkitMiddlewaremeans you do, which silently opts you out of that default. AddingcreateCsrfMiddlewareback restores it. It's a pure header check (Sec-Fetch-Site/Origin/Referer) with no tokens and no interaction with the AuthKit session cookie; list it beforeauthkitMiddlewareso cross-site requests are rejected before any session work runs. If you handle CSRF another way, omit it — Start will warn in dev, which you can silence withtanstackStart({ serverFns: { disableCsrfMiddlewareWarning: true } }).
Create src/routes/api/auth/callback.tsx:
import{createFileRoute}from'@tanstack/react-router';import{handleCallbackRoute}from'@workos/authkit-tanstack-react-start';exportconstRoute=createFileRoute('/api/auth/callback')({server: {handlers: {GET: handleCallbackRoute(),},},});Make sure this matches your WORKOS_REDIRECT_URI environment variable.
Create a route that initiates the AuthKit sign-in flow. This route is used as the Sign-in URL (also known as initiate_login_uri) in your WorkOS dashboard settings.
Create src/routes/api/auth/sign-in.tsx:
import{createFileRoute}from'@tanstack/react-router';import{getSignInUrl}from'@workos/authkit-tanstack-react-start';exportconstRoute=createFileRoute('/api/auth/sign-in')({server: {handlers: {GET: async({ request }: {request: Request})=>{constreturnPathname=newURL(request.url).searchParams.get('returnPathname');consturl=awaitgetSignInUrl(returnPathname ? {data: { returnPathname }} : undefined);returnnewResponse(null,{status: 307,headers: {Location: url},});},},},});In the WorkOS dashboard Redirects page, set the Sign-in URL to match this route (e.g., http://localhost:3000/api/auth/sign-in).
Important
The Sign-in URL is required for features like impersonation to work correctly. Without it, WorkOS-initiated flows (such as impersonating a user from the dashboard) will fail because they cannot complete the PKCE/CSRF verification that this library enforces on every callback.
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';exportconstRoute=createRootRoute({component: RootComponent,});functionRootComponent(){return(<AuthKitProvider><Outlet/></AuthKitProvider>);}If you're only using server-side authentication (getAuth() in loaders), you can skip this step.
Open the Redirects page in the WorkOS dashboard and configure:
- Redirect URIs — add your callback URL:
http://localhost:3000/api/auth/callback - Sign-in URL — set to the route from step 3 above:
http://localhost:3000/api/auth/sign-in. Required for WorkOS-initiated flows like dashboard impersonation. - Sign-out redirect — where to send users after sign-out. If unset, WorkOS falls back to the App homepage URL; if neither is set, WorkOS shows 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}from'@workos/authkit-tanstack-react-start';exportconstRoute=createFileRoute('/dashboard')({loader: async()=>{const{ user }=awaitgetAuth();if(!user){throwredirect({href: '/api/auth/sign-in'});}return{ user };},component: DashboardPage,});functionDashboardPage(){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 intentimport{useAuth}from'@workos/authkit-tanstack-react-start/client';functionProfileButton(){const{ user, loading, signOut }=useAuth();if(loading)return<div>Loading...</div>;if(!user)return<ahref="/signin">SignIn</a>;return(<div><span>{user.email}</span><buttononClick={()=>signOut()}>SignOut</button></div>);}Server-side (in route loader):
import{signOut}from'@workos/authkit-tanstack-react-start';exportconstRoute=createFileRoute('/logout')({loader: async()=>{awaitsignOut();// Redirects to WorkOS logout, then back to '/'},});Client-side (from useAuth hook):
const{ signOut }=useAuth();awaitsignOut({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 loaderconstauth=awaitswitchToOrganization({data: {organizationId: 'org_456'},});// Session now has org_456's role, permissions, etc.Client-side:
const{ switchToOrganization, organizationId }=useAuth();awaitswitchToOrganization('org_456');// Auth state updates automaticallyUse layout routes to protect multiple pages:
// src/routes/_authenticated.tsximport{createFileRoute,redirect}from'@tanstack/react-router';import{getAuth}from'@workos/authkit-tanstack-react-start';exportconstRoute=createFileRoute('/_authenticated')({loader: async({ location })=>{const{ user }=awaitgetAuth();if(!user){constreturnPathname=encodeURIComponent(location.pathname);throwredirect({href: `/api/auth/sign-in?returnPathname=${returnPathname}`});}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 }=awaitgetAuth();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.
awaitsignOut();awaitsignOut({data: {returnTo: '/goodbye'}});Options:
returnTo- Path to redirect to after logout (default:/)
Switches to a different organization and refreshes the session with new claims.
constauth=awaitswitchToOrganization({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 usageconsturl=awaitgetSignInUrl();// With return pathconsturl=awaitgetSignInUrl({data: {returnPathname: '/dashboard'},});Options:
returnPathname- Path to return to after sign-in
Generates a sign-up URL for redirecting to AuthKit.
consturl=awaitgetSignUpUrl();consturl=awaitgetSignUpUrl({data: {returnPathname: '/onboarding'},});Options:
returnPathname- Path to return to after sign-up
Advanced: Generate a custom authorization URL with full control.
consturl=awaitgetAuthorizationUrl({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';exportconstRoute=createFileRoute('/api/auth/callback')({server: {handlers: {GET: handleCallbackRoute(),},},});With a sign-in error page (browser-friendly default):
exportconstRoute=createFileRoute('/api/auth/callback')({server: {handlers: {GET: handleCallbackRoute({errorRedirectUrl: '/sign-in?error=auth_failed',}),},},});The user lands on /sign-in?error=auth_failed (a route you own) instead of seeing raw JSON. Verifier-delete cookies are still attached.
With Sentry capture:
import*asSentryfrom'@sentry/node';exportconstRoute=createFileRoute('/api/auth/callback')({server: {handlers: {GET: handleCallbackRoute({onSuccess: async({ user, authenticationMethod })=>{awaitdb.users.upsert({id: user.id,email: user.email});analytics.track('User Signed In',{method: authenticationMethod});},onError: ({ error, request })=>{Sentry.captureException(error,{extra: {url: request.url}});returnResponse.redirect(newURL('/sign-in?error=auth_failed',request.url));},}),},},});onError runs for every callback failure (missing code, state mismatch, token exchange failure, onSuccess throws). The SDK already emits a console.error for every failure, so if you wire Sentry's console.error ingestion you don't need to call Sentry.captureException yourself.
Options:
onSuccess?: (data) => Promise<void>— Called after successful authentication with user data, tokens, and authentication method.onError?: ({ error, request }) => Response— Custom error handler that returns a Response. Errors thrown from insideonErrorare NOT caught by the SDK.errorRedirectUrl?: string— URL (absolute or relative) to redirect to on callback failure whenonErroris not set. If both are set,onErrorwins. Set this at route-construction time only — do not derive from request input (it would be an open-redirect vector).returnPathname?: string— Override the success-path redirect after authentication. Does not apply to errors.
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';functionMyComponent(){const{ user, loading, signOut }=useAuth();if(loading)return<div>Loading...</div>;if(!user)return<div>Notsignedin</div>;return(<div><p>{user.email}</p><buttononClick={()=>signOut()}>SignOut</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';functionApiCaller(){const{ accessToken, loading, getAccessToken }=useAccessToken();constcallApi=async()=>{consttoken=awaitgetAccessToken();// Always freshconstresponse=awaitfetch('/api/data',{headers: {Authorization: `Bearer ${token}`},});};return<buttononClick={callApi}>FetchData</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';functionClaimsDisplay(){constclaims=useTokenClaims();if(!claims)returnnull;return(<div><p>SessionID: {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 usageauthkitMiddleware();// 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.
CSRF: Registering
authkitMiddlewareinrequestMiddlewareopts your app out of the CSRF middleware TanStack Start applies by default. Pair it withcreateCsrfMiddleware(from@tanstack/react-start) to protect your server-function RPC endpoints — see step 1 of setup.
This library is fully typed. Common types:
importtype{User,Session,UserInfo,NoUserInfo,Impersonator}from'@workos/authkit-tanstack-react-start';// User object from WorkOSconstuser: User={id: string;
email: string;
firstName: string|null;
lastName: string|null;
emailVerified: boolean;
profilePictureUrl: string|null;// ... more fields};// Auth result from getAuth()constauth: UserInfo|NoUserInfo=awaitgetAuth();Route loaders get full type inference:
exportconstRoute=createFileRoute('/profile')({loader: async()=>{const{ user }=awaitgetAuth();return{ user };// Fully typed},component: ProfilePage,});functionProfilePage(){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
Link to the Sign-in URL you created in setup step 3. The endpoint handles generating the AuthKit URL and setting the PKCE cookie.
exportconstRoute=createFileRoute('/')({loader: async()=>{const{ user }=awaitgetAuth();return{ user };},component: HomePage,});functionHomePage(){const{ user }=Route.useLoaderData();if(!user){return<ahref="/api/auth/sign-in">SignInwithAuthKit</a>;}return<div>Welcome,{user.firstName}!</div>;}// src/routes/_authenticated.tsximport{createFileRoute,redirect}from'@tanstack/react-router';import{getAuth}from'@workos/authkit-tanstack-react-start';exportconstRoute=createFileRoute('/_authenticated')({loader: async({ location })=>{const{ user }=awaitgetAuth();if(!user){constreturnPathname=encodeURIComponent(location.pathname);throwredirect({href: `/api/auth/sign-in?returnPathname=${returnPathname}`});}return{ user };},});// All child routes require authentication:// - _authenticated/dashboard.tsx// - _authenticated/settings.tsximport{useAuth}from'@workos/authkit-tanstack-react-start/client';functionOrgSwitcher(){const{ organizationId, switchToOrganization }=useAuth();return(<selectvalue={organizationId||''}onChange={(e)=>switchToOrganization(e.target.value)}><optionvalue="org_123">AcmeCorp</option><optionvalue="org_456">OtherCompany</option></select>);}Loader (server-side):
loader: async()=>{const{ user, organizationId, role }=awaitgetAuth();return{ user, organizationId, role };};Component (from loader data):
functionMyPage(){const{ user }=Route.useLoaderData();// ...}Client hook (reactive):
functionMyClientComponent(){const{ user, loading }=useAuth();// Updates on session changes}This error occurs when WorkOS-initiated flows (like dashboard impersonation) redirect directly to your callback URL without going through your application's sign-in flow. Because this library enforces PKCE/CSRF verification on every callback, the request is rejected when the required state parameter is missing.
Fix: Configure a Sign-in URL in your WorkOS dashboard so impersonation flows route through your app first, letting PKCE/state be set up before redirecting to WorkOS.
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 functionsimport{getAuth,signOut}from'@workos/authkit-tanstack-react-start';// Client hooksimport{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 }=awaitgetAuth();// ❌ Runs on client during hydration};Right:
loader: async()=>{const{ user }=awaitgetAuth();// ✅ 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