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