<!-- llms.txt: https://workos.com/llms.txt -->

## Installation

```bash
npm install @workos-inc/authkit-tanstack-start
```

## Usage

The AuthKit TanStack Start SDK provides utilities for integrating AuthKit authentication into TanStack Start applications.

Refer to the [AuthKit documentation](https://workos.com/docs/authkit) for detailed usage instructions and examples.


# AuthKit TanStack Start

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.

## Installation

```bash
npm install @workos/authkit-tanstack-react-start
```

```bash
pnpm add @workos/authkit-tanstack-react-start
```

## Quickstart

### Environment Variables

Create a `.env` file in your project root with the following required variables:

```bash
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):

```bash
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`:

```typescript
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 own
> `startInstance`. Registering `authkitMiddleware` means you do, which silently
> opts you out of that default. Adding `createCsrfMiddleware` back 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 before
> `authkitMiddleware` so 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 with `tanstackStart({ serverFns: { disableCsrfMiddlewareWarning: true } })`.

#### 2. Create Callback Route

Create `src/routes/api/auth/callback.tsx`:

```typescript
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`:

```typescript
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](https://dashboard.workos.com/redirects), 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](https://workos.com/docs/user-management/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`:

```typescript
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](https://dashboard.workos.com/redirects) in the WorkOS dashboard and configure:

1. **Redirect URIs** — add your callback URL: `http://localhost:3000/api/auth/callback`
2. **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.
3. **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.

## Usage

### Server-Side Authentication

Use `getAuth()` in route loaders or server functions to access the current session:

```typescript
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:

```typescript
'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):**

```typescript
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):**

```typescript
const { signOut } = useAuth();

await signOut({ returnTo: '/goodbye' });
```

### Organization Switching

Switch the active organization for multi-org users:

**Server-side:**

```typescript
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:**

```typescript
const { switchToOrganization, organizationId } = useAuth();

await switchToOrganization('org_456');
// Auth state updates automatically
```

### Protected Routes

Use layout routes to protect multiple pages:

```typescript
// 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.
```

## API Reference

### Server Functions

These functions can be called from route loaders, server functions, or server route handlers.

#### `getAuth()`

Retrieves the current user session.

```typescript
const { user } = await getAuth();

if (user) {
  console.log(user.email);
  console.log(user.firstName);
}
```

**Returns:** `UserInfo | NoUserInfo`

**UserInfo fields:**

- `user` - The authenticated user object
- `sessionId` - WorkOS session ID
- `organizationId` - Active organization (if in org context)
- `role` - User's role in the organization
- `roles` - Array of role strings
- `permissions` - Array of permission strings
- `entitlements` - Array of entitlement strings
- `featureFlags` - Array of feature flag strings
- `impersonator` - Impersonator details (if being impersonated)
- `accessToken` - JWT access token

#### `signOut(options?)`

Signs out the current user and redirects to WorkOS logout.

```typescript
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.

```typescript
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.

```typescript
// 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.

```typescript
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.

```typescript
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 authentication
- `redirectUri` - Override the default redirect URI

### Route Handlers

#### `handleCallbackRoute`

Handles the OAuth callback from WorkOS. Use this in your callback route.

**Basic usage:**

```typescript
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):**

```typescript
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:**

```typescript
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 inside `onError` are NOT caught by the SDK.
- `errorRedirectUrl?: string` — URL (absolute or relative) to redirect to on callback failure when `onError` is not set. If both are set, `onError` wins. 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.

```typescript
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 null
- `loading` - Loading state
- `sessionId`, `organizationId`, `role`, `roles`, `permissions`, `entitlements`, `featureFlags`, `impersonator`
- `getAuth()` - Refresh auth state
- `refreshAuth(options)` - Refresh session with optional org switch
- `signOut(options)` - Sign out
- `switchToOrganization(orgId)` - Switch organizations

#### `useAccessToken()`

Manage access tokens with automatic refresh.

```typescript
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 state
- `error` - Last error or null
- `refresh()` - Manually refresh token
- `getAccessToken()` - Get guaranteed fresh token

#### `useTokenClaims()`

Parse and decode JWT claims from the access token.

```typescript
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.

```typescript
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 from `WORKOS_REDIRECT_URI`. Useful for dynamic environments like preview deployments.

> **CSRF:** Registering `authkitMiddleware` in `requestMiddleware` opts your app
> out of the CSRF middleware TanStack Start applies by default. Pair it with
> `createCsrfMiddleware` (from `@tanstack/react-start`) to protect your
> server-function RPC endpoints — see [step 1 of setup](#1-configure-middleware).

## TypeScript

This library is fully typed. Common types:

```typescript
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:

```typescript
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!
}
```

## How It Works

### Server-Side Flow

1. **Middleware runs on every request** - validates/refreshes session, stores auth in context
2. **Route loaders call `getAuth()`** - retrieves auth from middleware context
3. **No client bundle bloat** - server functions create RPC boundaries automatically

### Client-Side Flow (with Provider)

1. **Provider wraps app** - provides auth context to hooks
2. **Hooks call server actions** - fetch auth state via RPC
3. **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

## Common Patterns

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

```typescript
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

```typescript
// 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

```typescript
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):**

```typescript
loader: async () => {
  const { user, organizationId, role } = await getAuth();
  return { user, organizationId, role };
};
```

**Component (from loader data):**

```typescript
function MyPage() {
  const { user } = Route.useLoaderData();
  // ...
}
```

**Client hook (reactive):**

```typescript
function MyClientComponent() {
  const { user, loading } = useAuth();
  // Updates on session changes
}
```

## Troubleshooting

### `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](#3-create-sign-in-endpoint) in your [WorkOS dashboard](https://dashboard.workos.com/redirects) 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:

1. Check your `.env` file exists
2. Verify all required variables are set
3. Ensure `WORKOS_COOKIE_PASSWORD` is 32+ characters
4. Restart your dev server after changing env vars

### Types not working / Import errors

Make sure you're importing from the right path:

```typescript
// 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:**

```typescript
beforeLoad: async () => {
  const { user } = await getAuth(); // ❌ Runs on client during hydration
};
```

**Right:**

```typescript
loader: async () => {
  const { user } = await getAuth(); // ✅ Server-only during SSR
};
```

Use `useAuth()` client hook for client components, or move logic to a loader.

## Example Application

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:

```bash
cd example
pnpm install
pnpm dev
```

## Framework Compatibility

- **TanStack Start:** v1.132.0+
- **TanStack Router:** v1.132.0+
- **React:** 18.0+
- **Node.js:** 18+

## Related

- [WorkOS AuthKit Documentation](https://workos.com/docs/user-management/authkit)
- [TanStack Start Documentation](https://tanstack.com/start/latest)
- [WorkOS Node SDK](https://github.com/workos/workos-node)

## License

MIT