AuthKit TanStack Start SDK
The AuthKit TanStack Start SDK provides utilities for integrating AuthKit authentication into TanStack Start applications.
Refer to the AuthKit documentation for detailed usage instructions and examples.
Beta Versions
Certain WorkOS features may be available only in the beta version of the SDK. Beta versions have the-beta.* suffix, for example,
3.2.0-beta.1. For more information on how to use beta
versions, refer to the README in the GitHub repository.
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-start
pnpm add @workos/authkit-tanstack-react-start
Environment Variables
Create 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 characters
Generate a secure cookie password (32+ characters):
openssl rand -base64 24
Optional Configuration
| 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 |
Setup (3 Steps)
1. Configure Middleware
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.
const csrfMiddleware = createCsrfMiddleware({
filter: (ctx) => ctx.handlerType === 'serverFn',
});
export const startInstance = 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 } }).
2. Create Callback Route
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.
3. Create Sign-In Endpoint
Create a route that initiates the AuthKit sign-in flow. This route is used as the Sign-in endpoint (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';
export const Route = createFileRoute('/api/auth/sign-in')({
server: {
handlers: {
GET: async ({ request }: { request: Request }) => {
const returnPathname = new URL(request.url).searchParams.get('returnPathname');
const url = await getSignInUrl(returnPathname ? { data: { returnPathname } } : undefined);
return new Response(null, {
status: 307,
headers: { Location: url },
});
},
},
},
});
In the WorkOS dashboard Redirects page, set the Sign-in endpoint to match this route (e.g., http://localhost:3000/api/auth/sign-in).
[!IMPORTANT] The sign-in endpoint 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.
4. Add Provider (Optional - only needed for client hooks)
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.
WorkOS Dashboard Configuration
Open the Redirects page in the WorkOS dashboard and configure:
- Redirect URIs — add your callback URL:
http://localhost:3000/api/auth/callback - Sign-in endpoint — 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.
Server-Side Authentication
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';
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const { user } = await getAuth();
if (!user) {
throw redirect({ href: '/api/auth/sign-in' });
}
return { user };
},
component: DashboardPage,
});
function DashboardPage() {
const { user } = Route.useLoaderData();
return <div>Welcome, {user.firstName}!</div>;
}
Client-Side Hooks
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>
);
}
Signing Out
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' });
Organization Switching
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 automatically
Protected Routes
Use layout routes to protect multiple pages:
// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router';
import { getAuth } from '@workos/authkit-tanstack-react-start';
export const Route = createFileRoute('/_authenticated')({
loader: async ({ location }) => {
const { user } = await getAuth();
if (!user) {
const returnPathname = encodeURIComponent(location.pathname);
throw redirect({ href: `/api/auth/sign-in?returnPathname=${returnPathname}` });
}
return { user };
},
});
// Now all routes under _authenticated require auth:
// - _authenticated/dashboard.tsx
// - _authenticated/profile.tsx
// etc.
Server Functions
These functions can be called from route loaders, server functions, or server route handlers.
getAuth()
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
signOut(options?)
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:/)
switchToOrganization(options)
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
getSignInUrl(options?)
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
getSignUpUrl(options?)
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
getAuthorizationUrl(options)
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
Route Handlers
handleCallbackRoute
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 a sign-in error page (browser-friendly default):
export const Route = 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 * as Sentry from '@sentry/node';
export const Route = createFileRoute('/api/auth/callback')({
server: {
handlers: {
GET: handleCallbackRoute({
onSuccess: async ({ user, authenticationMethod }) => {
await db.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 } });
return Response.redirect(new URL('/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.
Client Hooks
Available from @workos/authkit-tanstack-react-start/client. Requires <AuthKitProvider> wrapper.
useAuth(options?)
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
useAccessToken()
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
useTokenClaims()
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>
);
}
Middleware
authkitMiddleware(options?)
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.
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:
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!
}
Server-Side Flow
- 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
Client-Side Flow (with Provider)
- 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
Why the Provider is Optional
- 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
Sign In Flow
Link to the sign-in endpoint you created in setup step 3. The endpoint handles generating the AuthKit URL and setting the PKCE cookie.
export const Route = createFileRoute('/')({
loader: async () => {
const { user } = await getAuth();
return { user };
},
component: HomePage,
});
function HomePage() {
const { user } = Route.useLoaderData();
if (!user) {
return <a href="/api/auth/sign-in">Sign In with AuthKit</a>;
}
return <div>Welcome, {user.firstName}!</div>;
}
Protected Route Layout
// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router';
import { getAuth } from '@workos/authkit-tanstack-react-start';
export const Route = createFileRoute('/_authenticated')({
loader: async ({ location }) => {
const { user } = await getAuth();
if (!user) {
const returnPathname = encodeURIComponent(location.pathname);
throw redirect({ href: `/api/auth/sign-in?returnPathname=${returnPathname}` });
}
return { user };
},
});
// All child routes require authentication:
// - _authenticated/dashboard.tsx
// - _authenticated/settings.tsx
Organization Switcher
import { 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>
);
}
Accessing User in Multiple Places
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
}
Missing required auth parameter when impersonating from the WorkOS dashboard
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 endpoint in your WorkOS dashboard so impersonation flows route through your app first, letting PKCE/state be set up before redirecting to WorkOS.
”AuthKit middleware is not configured”
You forgot to add authkitMiddleware() to src/start.ts. See step 1 in setup.
”useAuth must be used within an AuthKitProvider”
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.
Environment variable errors on startup
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
Types not working / Import errors
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.
”can only be called on the server”
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