npm package: @hyvmind/authkit-remix-cloudflare
Description: Cloudflare-compatible auth and session helpers for using WorkOS & AuthKit with Remix
Latest version: 0.17.0

# AuthKit Remix Library (Cloudflare Workers/Pages)

The AuthKit library for Remix on Cloudflare provides convenient helpers for authentication and session management using WorkOS & AuthKit with Remix, purpose-built for Cloudflare Workers and Pages.

> **Note:** This is a Cloudflare-compatible fork of [@workos-inc/authkit-remix](https://github.com/workos/authkit-remix), matching upstream **v0.17.0**. The upstream library relies on `process.env` and Node.js APIs unavailable in Cloudflare Workers. This fork replaces those with Cloudflare-compatible alternatives using `context.cloudflare.env` and a centralized configuration system.

## Installation

Install the package with:

```
npm i @hyvmind/authkit-remix-cloudflare
```

or

```
yarn add @hyvmind/authkit-remix-cloudflare
```

## Configuration

Cloudflare Workers don't have a global `process.env`. Environment variables are delivered per-request through the loader's `context.cloudflare.env`. This library provides `configureFromContext()` to bridge that gap.

### 1. Environment Variables (`.dev.vars`)

Add your WorkOS credentials to a `.dev.vars` file in your project root (this is Cloudflare's equivalent of `.env`):

```sh
WORKOS_CLIENT_ID="client_..."              # retrieved from the WorkOS dashboard
WORKOS_API_KEY="sk_test_..."               # retrieved from the WorkOS dashboard
WORKOS_REDIRECT_URI="http://localhost:5173/callback" # configured in the WorkOS dashboard
WORKOS_COOKIE_PASSWORD="<your password>"   # generate a secure password here
```

`WORKOS_COOKIE_PASSWORD` is the private key used to encrypt the session cookie. It must be at least 32 characters long. You can generate one with:

```
openssl rand -base64 24
```

To use the `signOut` method, you'll need to set your app's homepage in your WorkOS dashboard settings under "Redirects".

### 2. Initialize from Context

The recommended approach is to call `configureFromContext()` in your root loader. This reads the `WORKOS_*` variables from `context.cloudflare.env` and makes them available to all AuthKit functions:

```ts
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
import { configureFromContext, authkitLoader } from '@hyvmind/authkit-remix-cloudflare';

export const loader = (args: LoaderFunctionArgs) => {
  configureFromContext(args.context);
  return authkitLoader(args);
};
```

### 3. Programmatic Configuration

You can also configure AuthKit programmatically using the `configure` function:

```ts
import { configure } from '@hyvmind/authkit-remix-cloudflare';

// Pass a config object with explicit values
configure({
  clientId: 'client_1234567890',
  apiKey: 'sk_test_1234567890',
  redirectUri: 'http://localhost:5173/callback',
  cookiePassword: 'your-secure-cookie-password-min-32-chars',
});
```

Or provide a custom value source function:

```ts
// Use a function to resolve environment variable names
configure((key) => context.cloudflare.env[key]);
```

Or combine both — explicit config values with a fallback source:

```ts
configure({ cookieName: 'my-custom-session' }, context.cloudflare.env);
```

### Configuration Priority

When retrieving configuration values, AuthKit follows this priority order:

1. Environment variables (via the value source set by `configure()` / `configureFromContext()`)
2. Programmatically provided values
3. Default values for optional settings

### Available Configuration Options

| Option           | Environment Variable     | Default               | Required | Description                                   |
| ---------------- | ------------------------ | --------------------- | -------- | --------------------------------------------- |
| `clientId`       | `WORKOS_CLIENT_ID`       | –                     | Yes      | Your WorkOS Client ID                         |
| `apiKey`         | `WORKOS_API_KEY`         | –                     | Yes      | Your WorkOS API Key                           |
| `redirectUri`    | `WORKOS_REDIRECT_URI`    | –                     | Yes      | The callback URL configured in WorkOS         |
| `cookiePassword` | `WORKOS_COOKIE_PASSWORD` | –                     | Yes      | Password for cookie encryption (min 32 chars) |
| `cookieName`     | `WORKOS_COOKIE_NAME`     | `wos-session`         | No       | Name of the session cookie                    |
| `apiHttps`       | `WORKOS_API_HTTPS`       | `true`                | No       | Whether to use HTTPS for API calls            |
| `cookieMaxAge`   | `WORKOS_COOKIE_MAX_AGE`  | `34560000` (400 days) | No       | Maximum age of cookie in seconds              |
| `apiHostname`    | `WORKOS_API_HOSTNAME`    | `api.workos.com`      | No       | WorkOS API hostname                           |
| `apiPort`        | `WORKOS_API_PORT`        | –                     | No       | Port to use for API calls                     |

#### Optional Environment Variables

These can also be set in `.dev.vars` for debugging or customization:

```sh
WORKOS_COOKIE_MAX_AGE='600'          # maximum age of the cookie in seconds
WORKOS_API_HOSTNAME='api.workos.com' # base WorkOS API URL
WORKOS_API_HTTPS=true                # whether to use HTTPS in API calls
WORKOS_API_PORT=3000                 # port to use for API calls
```

## Setup

### Callback Route

AuthKit requires a callback URL to redirect users back to after authentication. In your Remix app, [create a new route](https://remix.run/docs/en/main/discussion/routes) and add the following:

```ts
import { authLoader } from '@hyvmind/authkit-remix-cloudflare';

export const loader = authLoader();
```

Make sure this route matches the `WORKOS_REDIRECT_URI` variable and the configured redirect URI in your WorkOS dashboard. For instance, if your redirect URI is `http://localhost:5173/callback` then you'd put the above code in `/app/routes/callback.ts`.

You can control the pathname the user will be sent to after signing in by passing a `returnPathname` option:

```ts
export const loader = authLoader({ returnPathname: '/dashboard' });
```

If your application needs to persist `oauthTokens` or other auth-related information after a successful callback, you can pass an `onSuccess` option:

```ts
export const loader = authLoader({
  onSuccess: async ({ accessToken, refreshToken, user, oauthTokens, organizationId, impersonator }) => {
    await saveToDatabase(oauthTokens);
  },
});
```

### Root Loader

Initialize AuthKit in your root loader so the session is available across your entire application:

```ts
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
import { configureFromContext, authkitLoader } from '@hyvmind/authkit-remix-cloudflare';

export const loader = (args: LoaderFunctionArgs) => {
  configureFromContext(args.context);
  return authkitLoader(args);
};
```

## Usage

### Access Authentication Data

Use `authkitLoader` in any route to access authentication data. The returned data includes `user`, `sessionId`, `organizationId`, `role`, `roles`, `permissions`, `entitlements`, and `impersonator`.

```tsx
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
import { useLoaderData } from '@remix-run/react';
import { authkitLoader } from '@hyvmind/authkit-remix-cloudflare';

export const loader = (args: LoaderFunctionArgs) => authkitLoader(args);

export default function App() {
  const { user } = useLoaderData<typeof loader>();

  return (
    <div>
      <p>Welcome back{user?.firstName && `, ${user.firstName}`}</p>
    </div>
  );
}
```

### Custom Loader Functions

Pass a loader function to `authkitLoader` to combine auth data with your own data. The function receives the standard Remix loader args plus `auth` and `getAccessToken`:

```tsx
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
import { Form, Link, useLoaderData } from '@remix-run/react';
import { getSignInUrl, getSignUpUrl, signOut, authkitLoader } from '@hyvmind/authkit-remix-cloudflare';

export const loader = (args: LoaderFunctionArgs) =>
  authkitLoader(args, async ({ request, auth }) => {
    return {
      signInUrl: await getSignInUrl(),
      signUpUrl: await getSignUpUrl(),
    };
  });

export async function action({ request }: ActionFunctionArgs) {
  return await signOut(request);
}

export default function HomePage() {
  const { user, signInUrl, signUpUrl } = useLoaderData<typeof loader>();

  if (!user) {
    return (
      <>
        <Link to={signInUrl}>Log in</Link>
        <br />
        <Link to={signUpUrl}>Sign Up</Link>
      </>
    );
  }

  return (
    <Form method="post">
      <p>Welcome back{user?.firstName && `, ${user.firstName}`}</p>
      <button type="submit">Sign out</button>
    </Form>
  );
}
```

### Requiring Auth

For pages where a signed-in user is mandatory, use the `ensureSignedIn` option:

```ts
export const loader = (args: LoaderFunctionArgs) => authkitLoader(args, { ensureSignedIn: true });
```

Enabling `ensureSignedIn` will redirect unauthenticated users to AuthKit automatically. When used with a custom loader, `auth.user` is guaranteed to be non-null:

```ts
export const loader = (args: LoaderFunctionArgs) =>
  authkitLoader(
    args,
    async ({ auth, getAccessToken }) => {
      // auth.user is guaranteed to exist
      const accessToken = getAccessToken(); // always returns a string
      return { profile: await fetchProfile(accessToken) };
    },
    { ensureSignedIn: true },
  );
```

### Getting the Access Token

Access tokens are available through the `getAccessToken()` callback within your loader function. By design, access tokens are **not included** in the default response data — this prevents accidental exposure in browser developer tools, HTML source, or client-side logs.

```tsx
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
import { authkitLoader } from '@hyvmind/authkit-remix-cloudflare';

export const loader = (args: LoaderFunctionArgs) =>
  authkitLoader(args, async ({ auth, getAccessToken }) => {
    if (!auth.user) {
      return { data: null };
    }

    const accessToken = getAccessToken();

    const serviceData = await fetch('https://api.example.com/data', {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    });

    return {
      data: await serviceData.json(),
    };
  });
```

> **Security note:** Prefer making API calls server-side in your loaders rather than exposing access tokens to the client. If you must expose the token to client-side code, explicitly return it from your loader and consider using tokens with limited scope.

### Using `withAuth` for Lightweight Auth Checks

For advanced use cases, the `withAuth` function provides direct access to authentication data without the full loader machinery. Unlike `authkitLoader`, it:

- Does **not** handle automatic token refresh
- Does **not** manage cookies or session updates
- Returns the `accessToken` directly as a property
- Requires manual redirect handling for unauthenticated users

> **Important:** `withAuth` requires that `authkitLoader` is used in a parent or root route to handle session refresh and cookie management.

```tsx
import type { LoaderFunctionArgs } from '@remix-run/cloudflare';
import { redirect } from '@remix-run/cloudflare';
import { withAuth } from '@hyvmind/authkit-remix-cloudflare';

export const loader = async (args: LoaderFunctionArgs) => {
  const auth = await withAuth(args);

  if (!auth.user) {
    throw redirect('/sign-in');
  }

  const { accessToken, user, sessionId, organizationId, role, roles, permissions, entitlements } = auth;

  const apiData = await fetch('https://api.example.com/data', {
    headers: { Authorization: `Bearer ${accessToken}` },
  });

  return {
    user,
    apiData: await apiData.json(),
  };
};
```

**When to use `withAuth` vs `authkitLoader`:**

- Use **`authkitLoader`** for most cases — it handles token refresh, cookies, and provides safer defaults
- Use **`withAuth`** when you need more control or are building custom authentication flows (e.g., API routes)

### Organization Switching

Use `switchToOrganization` to switch the current session to a different organization:

```ts
import type { ActionFunctionArgs } from '@remix-run/cloudflare';
import { switchToOrganization } from '@hyvmind/authkit-remix-cloudflare';

export async function action({ request }: ActionFunctionArgs) {
  return await switchToOrganization(request, 'org_01234567890', {
    returnTo: '/dashboard', // optional redirect after switching
  });
}
```

If `returnTo` is provided, the user is redirected to that path with the updated session cookie. Otherwise, a JSON response with the updated auth data is returned.

### Signing Out

Use the `signOut` method to sign out the current user, end the session, and redirect to your app's homepage. The homepage redirect is set in your WorkOS dashboard settings under "Redirects".

```ts
import type { ActionFunctionArgs } from '@remix-run/cloudflare';
import { signOut } from '@hyvmind/authkit-remix-cloudflare';

export async function action({ request }: ActionFunctionArgs) {
  return await signOut(request);
}
```

To specify where the user is redirected after sign-out, pass an optional `returnTo` argument. Allowed values are configured in the WorkOS Dashboard under [Logout redirects](https://workos.com/docs/user-management/sessions/configuring-sessions/logout-redirect):

```ts
export async function action({ request }: ActionFunctionArgs) {
  return await signOut(request, { returnTo: 'https://example.com' });
}
```

### Debugging

To enable debug logs, pass in the `debug` flag:

```ts
export const loader = (args: LoaderFunctionArgs) => authkitLoader(args, { debug: true });
```

If providing a loader function, pass the options object as the third parameter:

```ts
export const loader = (args: LoaderFunctionArgs) =>
  authkitLoader(
    args,
    async ({ auth }) => {
      return { foo: 'bar' };
    },
    { debug: true },
  );
```

## Custom Session Storage

By default, AuthKit uses cookie-based session storage with these settings:

```ts
{
  name: "wos-session",
  path: "/",
  httpOnly: true,
  secure: true,       // when redirect URI uses HTTPS
  sameSite: "lax",
  maxAge: 34560000,    // 400 days (configurable via WORKOS_COOKIE_MAX_AGE)
  secrets: [/* WORKOS_COOKIE_PASSWORD */],
}
```

You can provide your own session storage implementation to both `authkitLoader` and `authLoader`:

```ts
import { createCookieSessionStorage } from '@remix-run/cloudflare';
import { authkitLoader, authLoader } from '@hyvmind/authkit-remix-cloudflare';

const customStorage = createCookieSessionStorage({
  cookie: {
    name: 'my-auth-session',
    secrets: ['my-secret'],
    sameSite: 'lax',
    path: '/',
    httpOnly: true,
    secure: true,
    maxAge: 60 * 60 * 24, // 1 day
  },
});

// In your root loader
export const loader = (args: LoaderFunctionArgs) =>
  authkitLoader(args, {
    storage: customStorage,
    cookie: { name: 'my-auth-session' },
  });

// In your callback route
export const loader = authLoader({
  storage: customStorage,
  cookie: { name: 'my-auth-session' },
});
```

For code reuse and consistency, consider a shared helper:

```ts
// app/lib/session.server.ts
import { createCookieSessionStorage } from '@remix-run/cloudflare';

export function getAuthStorage() {
  const storage = createCookieSessionStorage({
    cookie: {
      name: 'my-session',
      secrets: ['my-secret'],
      sameSite: 'lax',
      path: '/',
      httpOnly: true,
      secure: true,
    },
  });
  return { storage, cookie: { name: 'my-session' } };
}

// Then in your routes
import { getAuthStorage } from '~/lib/session.server';

export const loader = (args: LoaderFunctionArgs) =>
  authkitLoader(args, {
    ...getAuthStorage(),
  });
```

AuthKit works with any session storage that implements Remix's `SessionStorage` interface.

## Session Refresh Callbacks

You can hook into the session refresh lifecycle for logging, analytics, or custom error handling:

```ts
export const loader = (args: LoaderFunctionArgs) =>
  authkitLoader(args, {
    onSessionRefreshSuccess: async ({ accessToken, user, impersonator, organizationId }) => {
      console.log('Session refreshed for user:', user.id);
    },
    onSessionRefreshError: async ({ error, request, sessionData }) => {
      console.error('Session refresh failed:', error);
      // Optionally return a Response to override the default redirect behavior
      // return redirect('/custom-error-page');
    },
  });
```

- **`onSessionRefreshSuccess`** is called after a successful token refresh with the new `accessToken`, `user`, `impersonator`, and `organizationId`.
- **`onSessionRefreshError`** is called when a token refresh fails. If it returns a `Response`, that response is used instead of the default redirect to the sign-in page.

## Migration from v0.0.x

If you're upgrading from the earlier `0.0.x` versions of this Cloudflare fork, here are the breaking changes:

### `configureFromContext()` is now required

Previously, functions accepted `context` as a parameter. Now you must call `configureFromContext(context)` once (typically in your root loader) before using any AuthKit functions:

```ts
// Before (v0.0.x)
export const loader = (args: LoaderFunctionArgs) => authkitLoader(args);

// After (v0.17.x)
export const loader = (args: LoaderFunctionArgs) => {
  configureFromContext(args.context);
  return authkitLoader(args);
};
```

### Access tokens removed from default response

Access tokens are no longer included in the default `authkitLoader` response data. Use the `getAccessToken()` callback instead:

```ts
// Before (v0.0.x) — accessToken was in the response
const { accessToken } = useLoaderData<typeof loader>();

// After (v0.17.x) — use getAccessToken() in the loader
export const loader = (args: LoaderFunctionArgs) =>
  authkitLoader(args, async ({ auth, getAccessToken }) => {
    const accessToken = getAccessToken();
    return { data: await fetchData(accessToken) };
  });
```

### `getWorkos()` renamed to `getWorkOS()`

The function to access the WorkOS client instance has been renamed for consistency:

```ts
// Before
import { getWorkos } from '@hyvmind/authkit-remix-cloudflare';

// After
import { getWorkOS } from '@hyvmind/authkit-remix-cloudflare';
```

### New fields: `roles` and `entitlements`

The auth data now includes `roles` (array of role strings) and `entitlements` (array of entitlement strings) alongside the existing `role` and `permissions` fields.

### `signOut` signature simplified

`signOut` no longer requires the `context` parameter:

```ts
// Before (v0.0.x)
return await signOut(request, context);

// After (v0.17.x)
return await signOut(request);
return await signOut(request, { returnTo: 'https://example.com' });
```

## API Reference

| Export                                            | Type     | Description                                                                   |
| ------------------------------------------------- | -------- | ----------------------------------------------------------------------------- |
| `configureFromContext(context)`                   | Function | Initialize AuthKit from Remix's `AppLoadContext` (recommended for Cloudflare) |
| `configure(configOrSource, source?)`              | Function | Configure AuthKit with a config object, value source, or both                 |
| `getConfig(key)`                                  | Function | Retrieve a configuration value by key                                         |
| `authkitLoader(args, loaderOrOptions?, options?)` | Function | Authentication-aware route loader with session management                     |
| `authLoader(options?)`                            | Function | Callback route loader for handling the OAuth redirect                         |
| `getSignInUrl(returnPathname?)`                   | Function | Get the AuthKit sign-in URL                                                   |
| `getSignUpUrl(returnPathname?)`                   | Function | Get the AuthKit sign-up URL                                                   |
| `signOut(request, options?)`                      | Function | Sign out the user and redirect                                                |
| `switchToOrganization(request, orgId, options?)`  | Function | Switch the session to a different organization                                |
| `withAuth(args)`                                  | Function | Lightweight auth check without session refresh                                |
| `getWorkOS()`                                     | Function | Get the lazily-initialized WorkOS client instance                             |

## License

MIT