@hyvmind/authkit-remix-cloudflare
v0.17.0
Published
Cloudflare-compatible auth and session helpers for using WorkOS & AuthKit with Remix
Maintainers
Readme
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, matching upstream v0.17.0. The upstream library relies on
process.envand Node.js APIs unavailable in Cloudflare Workers. This fork replaces those with Cloudflare-compatible alternatives usingcontext.cloudflare.envand a centralized configuration system.
Installation
Install the package with:
npm i @hyvmind/authkit-remix-cloudflareor
yarn add @hyvmind/authkit-remix-cloudflareConfiguration
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):
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 hereWORKOS_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 24To 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:
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:
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:
// Use a function to resolve environment variable names
configure((key) => context.cloudflare.env[key]);Or combine both — explicit config values with a fallback source:
configure({ cookieName: 'my-custom-session' }, context.cloudflare.env);Configuration Priority
When retrieving configuration values, AuthKit follows this priority order:
- Environment variables (via the value source set by
configure()/configureFromContext()) - Programmatically provided values
- 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:
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 callsSetup
Callback Route
AuthKit requires a callback URL to redirect users back to after authentication. In your Remix app, create a new route and add the following:
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:
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:
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:
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.
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:
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:
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:
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.
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
accessTokendirectly as a property - Requires manual redirect handling for unauthenticated users
Important:
withAuthrequires thatauthkitLoaderis used in a parent or root route to handle session refresh and cookie management.
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
authkitLoaderfor most cases — it handles token refresh, cookies, and provides safer defaults - Use
withAuthwhen 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:
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".
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:
export async function action({ request }: ActionFunctionArgs) {
return await signOut(request, { returnTo: 'https://example.com' });
}Debugging
To enable debug logs, pass in the debug flag:
export const loader = (args: LoaderFunctionArgs) => authkitLoader(args, { debug: true });If providing a loader function, pass the options object as the third parameter:
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:
{
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:
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:
// 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:
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');
},
});onSessionRefreshSuccessis called after a successful token refresh with the newaccessToken,user,impersonator, andorganizationId.onSessionRefreshErroris called when a token refresh fails. If it returns aResponse, 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:
// 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:
// 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:
// 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:
// 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
