# Authentication for React Router or Remix Applications

Remix and React Router merged in 2024. What was planned as Remix v3 shipped as React Router v7 in December 2024, and that's now the recommended path for every new app. Auth in these frameworks lives in loaders, actions, and [middleware](https://clerk.com/glossary.md#middleware), so the primitives you pick on day one shape how cleanly the rest of the app runs.

This guide walks through adding authentication to a React Router v7 application using Clerk. By the end you'll have email + one-time code, Google and GitHub sign-in, protected routes in loaders and actions, a user menu, and organizations with admin/member roles. Everything uses TypeScript and the current `@clerk/react-router` SDK.

Not covered here: SAML and enterprise SSO flows (Clerk supports them; they need their own walkthrough), fully custom auth UI that replaces Clerk's components, and deep production infra decisions beyond per-platform notes.

## The short answer: best auth provider for a Remix app

For a React Router v7 or Remix v2 app, Clerk is the best-fit [managed authentication](https://clerk.com/glossary.md#authentication) provider. `@clerk/react-router` ships with three primitives that plug directly into the framework: `clerkMiddleware()` runs before every loader and action, `rootAuthLoader()` hydrates auth state server-side, and `getAuth()` reads the current session inside any loader or action. The package also bundles prebuilt, accessible UI components (`<SignIn />`, `<SignUp />`, `<UserButton />`, `<UserProfile />`, `<OrganizationSwitcher />`) so you don't build sign-in forms from scratch.

Honest alternatives exist and some make sense depending on your constraints. [Supabase Auth](https://supabase.com/docs/guides/auth) is cheapest at 100K users if you're already on Supabase. [Auth0](https://auth0.com/) and [WorkOS](https://workos.com/) are the right call when you need [SAML](https://clerk.com/glossary.md#security-assertion-markup-language-saml) and [SCIM](https://clerk.com/glossary.md#scim) out of the gate for enterprise customers. [remix-auth](https://github.com/sergiodxa/remix-auth) is a strategy-based library for teams that want TypeScript-first control and don't need prebuilt UI or built-in [MFA](https://clerk.com/glossary.md#multi-factor-authentication-mfa).

Each alternative trades something. Supabase doesn't ship passkeys and archived its prebuilt auth UI in October 2025. [Auth0 runs about $3,500/mo at 100K MAU on the Professional tier](https://auth0.com/pricing). WorkOS is free to 1M users but is B2B-focused. remix-auth doesn't include MFA, passkeys, or prebuilt UI and its social-provider plugins are partially broken on React Router v7. remix-auth works fine for email + password, but you rebuild every flow yourself.

For most React Router teams shipping a SaaS product with a small-to-medium team and a reasonable budget, Clerk is the default. The rest of this article shows why and how.

## Remix v2 vs React Router v7: what this guide covers

The Remix team merged their framework into React Router. What was scoped as Remix v3 shipped as [React Router v7](https://remix.run/blog/react-router-v7) in December 2024, and the Remix team now works on React Router v7 as the successor. Remix v2 is a supported legacy path.

Clerk has two SDKs:

1. [`@clerk/react-router`](https://www.npmjs.com/package/@clerk/react-router): actively developed, [targets React Router v7.1.2+ in framework mode](https://clerk.com/docs/react-router/getting-started/quickstart.md). **Use this for new apps.**
2. [`@clerk/remix`](https://www.npmjs.com/package/@clerk/remix): in maintenance mode (security updates only), for apps still on Remix v2.

If you're starting fresh, use React Router v7 and `@clerk/react-router`. If you have a Remix v2 app, you have two choices: migrate to React Router v7 (it's mostly import renames, and there's an [official codemod](https://reactrouter.com/upgrading/remix)), or stay on Remix v2 and use `@clerk/remix`. This guide targets React Router v7 throughout.

> React Router v7 has three modes: declarative, data, and framework. This article covers **framework mode**, which gives you file-based routing, loaders, actions, SSR by default, and the Vite plugin. If you're in declarative mode (SPA-only), most patterns still apply, but you won't have server-side loaders.

## Authentication options for Remix applications

Three broad paths: build it yourself, use a library like `remix-auth`, or use a managed provider. Each comes with its own tradeoffs.

### Option 1: DIY with session cookies and password hashing

Roll your own using Remix's `createCookieSessionStorage` plus `bcrypt` or `argon2` for password hashing. Realistic time to build the basics (email + password, one social provider, MFA, password reset): 40–60 hours before you're done. You get full control and no per-user cost.

You also own every OWASP concern: [session management](https://clerk.com/glossary.md#session-management), session fixation, rotation, breach detection, [rate limiting](https://clerk.com/glossary.md#rate-limiting), [bot detection](https://clerk.com/glossary.md#bot-detection), [email verification](https://clerk.com/glossary.md#verified-email), [CSRF](https://clerk.com/glossary.md#cross-site-request-forgery-csrf) tokens, and account recovery. You build the UI, too. Good fit for teams with a security specialist and a hard cost ceiling.

### Option 2: remix-auth with strategy packages

[`remix-auth`](https://github.com/sergiodxa/remix-auth) is a strategy-based library. Current stable is v4.2.0, [\~74k weekly downloads](https://www.npmjs.com/package/remix-auth), and it works with React Router v7. You write an `Authenticator` and plug in strategies: `FormStrategy`, `OAuth2Strategy`, and so on.

It's flexible and typed and free. You still own session management, storage, and UI. The `remix-auth-socials` V3 release is beta/broken for several providers on React Router v7 (Discord, LinkedIn, X), and `remix-auth-clerk` hasn't seen meaningful updates. No prebuilt UI, no MFA, no passkeys. Good fit for developers who want library-level control and are okay without [step-up auth](https://clerk.com/glossary.md#step-up-authentication) or hosted UI.

### Option 3: Managed authentication providers

Prebuilt UI, hosted sessions, SOC 2 (and often HIPAA) compliance, and [SDKs](https://clerk.com/glossary.md#software-development-kit-sdk) you install with one command. You trade control for speed and safety.

1. **Clerk**: best DX for the React/React Router ecosystem. 50,000 [MRU](https://clerk.com/glossary.md#monthly-retained-users-mrus) on the free plan. Prebuilt UI, organizations, bot detection, and passkeys included. MFA and passkeys on Pro.
2. **Auth0**: enterprise-grade SAML and SCIM. Around $3,500/mo at 100K MAU on the Professional tier. Mature but expensive. No dedicated React Router SDK.
3. **Supabase Auth**: [cheapest at scale (\~$188/mo at 100K MAU)](https://supabase.com/pricing) and tightly integrated with Supabase Postgres. No [passkeys](https://clerk.com/glossary.md#passkeys), no SAML, and the prebuilt Auth UI was archived in October 2025.
4. **WorkOS**: [free up to 1M users](https://workos.com/pricing). B2B-focused: SAML, SCIM, and organizations are first-class. Purpose-built for selling to enterprise customers from day one.

### MRU vs MAU: a note on billing metrics

Clerk bills on **Monthly Retained Users (MRU)**, defined as a user who returns to your app 24+ hours after signing up in a given month. Supabase, Auth0, and WorkOS bill on Monthly Active Users (MAU). MRU is narrower (signup-only visits don't count), so the metric is typically lower than MAU for the same app. Use each provider's own metric when comparing prices.

### How to choose

1. Need speed, polished UI, and organizations at small-to-medium scale → Clerk.
2. Need the cheapest option at 100K+ users and you're already on Supabase → Supabase Auth.
3. Need SAML/SCIM day one for enterprise sales → WorkOS, Auth0 or Clerk.
4. Need full control and no third party in the loop → DIY or remix-auth.

## Why Clerk fits Remix and React Router natively

Clerk's React Router SDK was built around the framework's model. Three things line up directly with how React Router works:

**A unified SDK across Remix and React Router.** `@clerk/react-router` targets React Router v7 framework mode. Remix v2 apps that have migrated to the v7 code path use the same SDK. Single source of truth for auth across both framework generations.

**Built around loaders, actions, and SSR.** `clerkMiddleware()` runs before every loader and action so auth state is ready when your handler runs. `rootAuthLoader()` hydrates auth state server-side in `root.tsx`, which means `<ClerkProvider>` is never "loading" on first paint. `getAuth(args)` reads the session inside any loader or action with the same signature.

Here's the shape of what that looks like:

```tsx
// app/root.tsx (abbreviated)
import { ClerkProvider } from '@clerk/react-router'
import { clerkMiddleware, rootAuthLoader } from '@clerk/react-router/server'

export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()]
export const loader = (args: Route.LoaderArgs) => rootAuthLoader(args)

export default function App({ loaderData }: Route.ComponentProps) {
  return <ClerkProvider loaderData={loaderData}>{/* app */}</ClerkProvider>
}
```

**Prebuilt, accessible UI components.** `<SignIn />`, `<SignUp />`, `<UserButton />`, `<UserProfile />`, and `<OrganizationSwitcher />` render into your layout. Style them with `appearance.variables` (design tokens), `appearance.elements` (per-element class maps), or full theme objects. Keyboard navigation, screen-reader labels, and focus management are handled.

**Hosted session infrastructure.** [Session JWTs](https://clerk.com/glossary.md#json-web-token) last 60 seconds and are auto-refreshed every 50 seconds (10 seconds of slack for network latency). Two [cookies](https://clerk.com/glossary.md#httponly-cookies) are in play: a short-lived `__session` JWT on your app domain and a long-lived HttpOnly `__client` cookie on Clerk's Frontend API domain. A handshake redirect refreshes the session server-side on expiry. Clerk also issues JWTs you can use to call your own APIs with `getToken()`.

**Everything in the box for most apps.** [OAuth](https://clerk.com/glossary.md#oauth) with 30+ providers, email + [one-time passcodes](https://clerk.com/glossary.md#one-time-passcodes-email-sms), [organizations](https://clerk.com/glossary.md#organizations) (admin/member roles, 100 [MROs](https://clerk.com/glossary.md#monthly-retained-organizations-mros) included, 20 members per org) on the free plan. MFA (TOTP, SMS, backup codes) and passkeys are included on Pro ($25/mo, or $20/mo billed annually) after [Clerk's February 2026 plan restructure](https://clerk.com/changelog/2026-02-05-new-plans-more-value.md) absorbed the former Enhanced Authentication add-on. Custom roles, unlimited org members, Verified Domains, Auto Invitations, and Enterprise SSO scoped to organizations require the separate B2B Authentication add-on ($100/mo, or $85/mo billed annually).

## Setting up a new Remix app with Clerk

Full walkthrough, copy-paste friendly. If you already have a React Router v7 app, jump to step 3.

### Prerequisites

- [ ] Node.js 20+ (LTS recommended)
- [ ] `npm`, `pnpm`, or `bun`
- [ ] A free Clerk account
- [ ] (For production) Google and/or GitHub OAuth credentials

### 1. Create the React Router v7 project

Run the official `create-react-router` CLI:

```bash
npx create-react-router@latest auth-app
cd auth-app
npm install
```

The generator scaffolds `app/root.tsx`, `app/routes.ts`, a default `app/routes/home.tsx`, and the `react-router.config.ts` file you'll edit in step 5.

### 2. Create a Clerk application

Open the [Clerk dashboard](https://dashboard.clerk.com), click [**Create application**](https://dashboard.clerk.com/apps/new), and pick your identifiers and social providers. For this walkthrough, enable **Email address**, **Google**, and **GitHub**. Copy the two keys that appear on the next screen; you'll paste them into `.env.local` in step 4.

### 3. Install `@clerk/react-router`

One package:

```bash
npm install @clerk/react-router
```

### 4. Add environment variables

Create `.env.local` in the project root and paste the [keys](https://dashboard.clerk.com/~/api-keys) from the Clerk dashboard:

```bash
VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxx
CLERK_SECRET_KEY=sk_test_xxx
```

The `VITE_` prefix is required for Vite to expose the value to the client. The [secret key](https://clerk.com/glossary.md#secret-key) has no prefix and stays on the server.

### 5. Enable the middleware future flag

React Router v7 puts middleware behind a stable future flag, `v8_middleware`, which [became stable in React Router v7.9.0](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md) (September 2025). Edit `react-router.config.ts`:

```ts
import type { Config } from '@react-router/dev/config'

export default {
  ssr: true,
  future: {
    v8_middleware: true,
  },
} satisfies Config
```

> If you're on React Router v7.3.0–v7.8.x, the flag was called `unstable_middleware`. Upgrade to v7.9.0+ and use `v8_middleware`; `@clerk/react-router` targets the stable flag.

### 6. Wire up `app/root.tsx`

Three exports do the heavy lifting: `middleware`, `loader`, and a `<ClerkProvider>` around your `<Outlet />`. The full file:

```tsx
// app/root.tsx
import { ClerkProvider, SignInButton, SignUpButton, Show, UserButton } from '@clerk/react-router'
import { clerkMiddleware, rootAuthLoader } from '@clerk/react-router/server'
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'
import type { Route } from './+types/root'

export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()]
export const loader = (args: Route.LoaderArgs) => rootAuthLoader(args)

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  )
}

export default function App({ loaderData }: Route.ComponentProps) {
  return (
    <ClerkProvider loaderData={loaderData}>
      <header className="flex items-center justify-end gap-2 p-4">
        <Show when="signed-out">
          <SignInButton />
          <SignUpButton />
        </Show>
        <Show when="signed-in">
          <UserButton />
        </Show>
      </header>
      <Outlet />
    </ClerkProvider>
  )
}

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  let message = 'Oops!'
  let details = 'An unexpected error occurred.'

  if (isRouteErrorResponse(error)) {
    message = error.status === 404 ? '404' : 'Error'
    details =
      error.status === 404 ? 'The requested page could not be found.' : error.statusText || details
  }

  return (
    <main className="p-4">
      <h1>{message}</h1>
      <p>{details}</p>
    </main>
  )
}
```

Two things to notice. First, `clerkMiddleware()` is exported as an array; React Router middleware is always an array export, even with a single entry. Second, `loaderData` flows from the `rootAuthLoader` return value through React Router's type-generated `Route.ComponentProps` into `<ClerkProvider>`, which is how auth state gets hydrated on the first render.

### 7. Verify the app boots signed out

Start the dev server:

```bash
npm run dev
```

Open `http://localhost:5173`. You should see the header with **Sign in** and **Sign up** buttons. Clicking either opens Clerk's modal. Sign up with an email address, confirm the OTP, and the header switches to show `<UserButton />`. If the buttons are missing or the page errors, jump to the troubleshooting section.

## Adding authentication UI

Out-of-the-box modals (via `<SignInButton />` / `<SignUpButton />`) work. For most apps you'll want dedicated sign-in and sign-up routes so the URLs are bookmarkable and the flows can own the full page.

### A dedicated sign-in route

Clerk's sign-in component is mounted at a splat route so it can handle its internal sub-paths (OAuth callbacks, two-factor challenges, etc.). Create `app/routes/sign-in.tsx`:

```tsx
// app/routes/sign-in.tsx
import { SignIn } from '@clerk/react-router'

export default function SignInPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignIn />
    </div>
  )
}
```

Register it in `app/routes.ts` as a splat:

```ts
// app/routes.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes'

export default [
  index('routes/home.tsx'),
  route('sign-in/*', 'routes/sign-in.tsx'),
  route('sign-up/*', 'routes/sign-up.tsx'),
] satisfies RouteConfig
```

Then tell Clerk to use these URLs in `.env.local`:

```bash
VITE_CLERK_SIGN_IN_URL=/sign-in
VITE_CLERK_SIGN_UP_URL=/sign-up
VITE_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/
VITE_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/
```

### A dedicated sign-up route

Same pattern. Create `app/routes/sign-up.tsx`:

```tsx
// app/routes/sign-up.tsx
import { SignUp } from '@clerk/react-router'

export default function SignUpPage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <SignUp />
    </div>
  )
}
```

The `sign-up/*` entry is already in `app/routes.ts` from the previous step.

### `<UserButton />` for the account menu

Already wired up in `root.tsx`. Useful props: `afterSignOutUrl` (where to go after sign-out), `userProfileMode` (`"modal"` or `"navigation"`), and `appearance` for customization. `<UserButton />` shows the user's avatar with a dropdown that includes account management, sign-out, and (if enabled) organization switching.

### `<UserProfile />` for self-service account management

Drop the `<UserProfile />` component onto its own route to let users manage email, password, MFA, connected accounts, and sessions without you building any of it:

```tsx
// app/routes/user-profile.tsx
import { UserProfile } from '@clerk/react-router'

export default function UserProfilePage() {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <UserProfile />
    </div>
  )
}
```

Register `route('user-profile/*', 'routes/user-profile.tsx')` the same way as the sign-in route. The page covers everything: profile data, passkeys, connected accounts, active [sessions](https://clerk.com/glossary.md#session), MFA setup, and sign-out.

### Configuring email + one-time code

In the Clerk dashboard:

1. Go to **Configure → User & authentication** and open the [**Email**](https://dashboard.clerk.com/~/user-authentication/user-and-authentication?user_auth_tab=email) tab.
2. Enable **Email address** as an identifier.
3. Under verification methods, enable **Email verification code**.
4. Save.

No code changes needed. `<SignIn />` and `<SignUp />` will show the email + OTP flow automatically.

### Configuring Google and GitHub social login

Also in the dashboard:

1. Go to **Configure → User & authentication → SSO connections** and select the [**Social**](https://dashboard.clerk.com/~/user-authentication/sso-connections/social) tab.
2. Toggle **Google**. In development you can use Clerk's shared OAuth credentials for fast iteration. For production, add your own Client ID and Secret from [Google Cloud Console](https://console.cloud.google.com/).
3. Toggle **GitHub**. Same deal: shared creds in dev, your own in production.
4. Save.

`<SignIn />` and `<SignUp />` render the enabled providers as buttons. No code change required.

## Protecting routes in Remix

Two protection surfaces exist in a React Router app: server-side (loaders, actions) and client-side (UI gating). Always protect the server side. Client-side gates are for UX only; they can't stop someone from hitting a loader URL directly.

### Server-side protection with `getAuth()` in a loader

The canonical pattern for any SSR React Router app:

```tsx
// app/routes/dashboard.tsx
import { getAuth } from '@clerk/react-router/server'
import { redirect } from 'react-router'
import type { Route } from './+types/dashboard'

export async function loader(args: Route.LoaderArgs) {
  const { isAuthenticated, userId } = await getAuth(args)

  if (!isAuthenticated) {
    throw redirect('/sign-in?redirect_url=' + args.request.url)
  }

  return { userId }
}

export default function Dashboard({ loaderData }: Route.ComponentProps) {
  return <h1>Hello, {loaderData.userId}</h1>
}
```

Notice `throw redirect(...)`: React Router treats thrown `Response` objects as loader bailouts. Using `throw` (not `return`) short-circuits the rest of the loader cleanly. The `redirect_url` query param lets the sign-in flow return the user to their original destination after auth.

### Protecting mutations inside actions

Same signature, same check:

```tsx
// app/routes/notes.new.tsx
import { getAuth } from '@clerk/react-router/server'
import { redirect } from 'react-router'
import type { Route } from './+types/notes.new'

export async function action(args: Route.ActionArgs) {
  const { isAuthenticated, userId } = await getAuth(args)

  if (!isAuthenticated) {
    throw redirect('/sign-in')
  }

  const formData = await args.request.formData()
  const content = formData.get('content')?.toString() ?? ''

  // ... save note with userId
  return { ok: true }
}
```

Every mutation needs this check. Don't rely on the UI hiding the form; a curl request hits the action regardless.

### Client-side UI gating with `<Show>`

For hiding or showing parts of the UI based on auth state, `<Show>` is the primary component in [Clerk Core 3](https://clerk.com/changelog/2026-03-03-core-3.md):

```tsx
import { Show, SignInButton } from '@clerk/react-router'

export default function Home() {
  return (
    <>
      <Show when="signed-in">
        <DashboardWidget />
      </Show>
      <Show when="signed-out">
        <SignInButton />
      </Show>
    </>
  )
}
```

The legacy `<SignedIn>` / `<SignedOut>` components still work for projects on older Core versions, but new code should use `<Show>`. It's one component with a typed `when` prop and a consistent API for authentication, roles, permissions, plans, and features.

## Working with authentication in loaders and actions

`getAuth(args)` returns more than just `userId`. The full shape covers everything you usually need on the server.

### Reading `userId` and session claims

The `Auth` object includes `userId`, `sessionId`, `sessionClaims`, `orgId`, `orgRole`, `orgSlug`, `has()`, and `getToken()`:

```tsx
// app/routes/settings.tsx
import { getAuth } from '@clerk/react-router/server'
import { redirect } from 'react-router'
import type { Route } from './+types/settings'

export async function loader(args: Route.LoaderArgs) {
  const { isAuthenticated, userId, sessionClaims, orgId } = await getAuth(args)

  if (!isAuthenticated) throw redirect('/sign-in')

  return {
    userId,
    email: sessionClaims?.email,
    orgId,
  }
}
```

`sessionClaims` is the decoded JWT payload. The default claim shape is minimal (ID, org context); add more via [JWT templates](https://dashboard.clerk.com/~/jwt-templates) ([seedocs](https://clerk.com/docs/guides/sessions/jwt-templates.md)) in the Clerk dashboard.

### Fetching the full User object

If you need profile data (name, email addresses, public metadata, etc.), reach for the Backend SDK via the `clerkClient` helper that ships with `@clerk/react-router/server`:

```tsx
// app/routes/profile.tsx
import { clerkClient, getAuth } from '@clerk/react-router/server'
import { redirect } from 'react-router'
import type { Route } from './+types/profile'

export async function loader(args: Route.LoaderArgs) {
  const { isAuthenticated, userId } = await getAuth(args)
  if (!isAuthenticated) throw redirect('/sign-in')

  const user = await clerkClient(args).users.getUser(userId)

  return {
    firstName: user.firstName,
    primaryEmail: user.primaryEmailAddress?.emailAddress,
  }
}
```

`clerkClient(args)` returns a pre-configured Backend SDK instance scoped to the current request. You can also list users, update metadata, create organization memberships, and anything else the Backend SDK exposes.

### Calling your own API with a Clerk-issued JWT

Client-side, `useAuth().getToken()` returns a short-lived JWT you attach to outbound requests:

```tsx
// app/routes/some-client-page.tsx
import { useAuth } from '@clerk/react-router'

export function CallMyApi() {
  const { getToken } = useAuth()

  async function handleClick() {
    const token = await getToken()
    // Replace with your own API URL.
    await fetch('https://api.example.com/things', {
      headers: { Authorization: `Bearer ${token}` },
    })
  }

  return <button onClick={handleClick}>Call API</button>
}
```

Server-side (in your own API, not in the same Remix app), verify the token with `verifyToken` from `@clerk/backend`:

```ts
// external-api/verify.ts
import { verifyToken } from '@clerk/backend'

export async function authenticate(authHeader: string | null) {
  const token = authHeader?.replace('Bearer ', '')
  if (!token) throw new Error('Missing token')

  const payload = await verifyToken(token, {
    secretKey: process.env.CLERK_SECRET_KEY!,
  })
  return payload.sub // Clerk user ID
}
```

The default token expires in 60 seconds. If your API needs custom claims or a longer-lived token for a specific integration, configure a [JWT template](https://dashboard.clerk.com/~/jwt-templates) in the Clerk dashboard and call `getToken({ template: 'my-template' })`.

## Organizations and role-based access

Clerk's organizations feature gives you multi-tenant B2B primitives on the free plan: admin/member [roles](https://clerk.com/glossary.md#roles), memberships, invites, and switching.

### Enabling organizations in the Clerk dashboard

1. Open the dashboard.
2. Navigate to **Configure → Organizations → [Settings](https://dashboard.clerk.com/~/organizations-settings)** and toggle organizations on.
3. (Optional) Configure which users can create organizations.
4. Save.

Free plan limits: 100 MROs (Monthly Retained Organizations) included, 20 members per org, and the two built-in roles (`org:admin`, `org:member`). The Pro plan keeps the same organization limits unless you add the B2B Authentication add-on.

### Using the `<OrganizationSwitcher />` component

One line in your header lets users create, switch, and manage organizations:

```tsx
// app/root.tsx (header section)
import { OrganizationSwitcher, Show } from '@clerk/react-router'

export function AppHeader() {
  return (
    <header>
      <Show when="signed-in">
        <OrganizationSwitcher />
      </Show>
    </header>
  )
}
```

The component shows the user's personal account, their organizations, and controls to create or leave orgs.

### The built-in admin and member roles

Every user in an organization has exactly one role: `org:admin` or `org:member`. Admins can invite new members, remove members, and manage billing (if billing is enabled). Members can use the app inside the org but can't administer it.

### Gating routes and UI by role

Server-side, use the `has()` helper returned from `getAuth()`:

```tsx
// app/routes/org-admin.tsx
import { getAuth } from '@clerk/react-router/server'
import { redirect } from 'react-router'
import type { Route } from './+types/org-admin'

export async function loader(args: Route.LoaderArgs) {
  const { isAuthenticated, has } = await getAuth(args)

  if (!isAuthenticated) throw redirect('/sign-in')
  if (!has({ role: 'org:admin' })) throw redirect('/')

  return null
}

export default function OrgAdmin() {
  return <h1>Org admin panel</h1>
}
```

Client-side, `<Show>` takes the same role predicate:

```tsx
import { Show } from '@clerk/react-router'

export function AdminSettings() {
  return (
    <Show when={{ role: 'org:admin' }}>
      <button>Delete organization</button>
    </Show>
  )
}
```

### Custom roles and permissions via the B2B Authentication add-on

The base plans ship the `org:admin` / `org:member` pair only. Custom roles (designer, billing\_manager, etc.), custom permissions (`org:invoices:create`), Rolesets, unlimited members per organization, Verified Domains, Auto Invitations, and Enterprise SSO scoped to organizations require Clerk's B2B Authentication add-on ($100/mo monthly, $85/mo billed annually). The add-on sits on top of either Free or Pro. Without it, you stay on the built-in role pair. See [Clerk Pricing](https://clerk.com/pricing) and [Roles and Permissions](https://clerk.com/docs/guides/organizations/control-access/roles-and-permissions.md) for the full matrix.

## Adding Clerk to an existing Remix or React Router application

If you already have an app, you're not starting from zero. The migration is small and incremental.

### Migration checklist

1. Install `@clerk/react-router`.
2. Set `VITE_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY`.
3. Enable `future: { v8_middleware: true }` in `react-router.config.ts`.
4. Export `middleware = [clerkMiddleware()]` from `app/root.tsx`.
5. Add a `loader` that calls `rootAuthLoader(args)` to `app/root.tsx`.
6. Wrap your `<Outlet />` with `<ClerkProvider loaderData={loaderData}>`.
7. Create splat routes for `/sign-in` and `/sign-up` with `<SignIn />` and `<SignUp />`.
8. Replace existing auth checks in loaders and actions with `getAuth(args)`.
9. Run your users through [Clerk's user migration tooling](https://clerk.com/docs/guides/development/migrating/overview.md) or trickle migration on sign-in.

### Replacing remix-auth strategies

If you were using `remix-auth`, the migration is mostly mechanical:

| Before (remix-auth)                                  | After (Clerk)                         |
| ---------------------------------------------------- | ------------------------------------- |
| `FormStrategy` (email/password)                      | `<SignIn />`                          |
| OAuth strategies (Google, GitHub, etc.)              | Enable providers in Clerk dashboard   |
| `authenticator.isAuthenticated(request)`             | `await getAuth(args)`                 |
| `authenticator.logout(request, { redirectTo: '/' })` | `await signOut({ redirectUrl: '/' })` |

All the strategy-specific code disappears. OAuth providers move from app code to dashboard toggles.

### Running Clerk alongside an existing session

During a phased migration you can feature-flag which users go through Clerk. In a loader:

```ts
// app/lib/auth.ts
import { getAuth } from '@clerk/react-router/server'
import type { LoaderFunctionArgs } from 'react-router'
import { getLegacySession, isClerkEnabledForUser } from './legacy-session'

export async function getCurrentUser(args: LoaderFunctionArgs) {
  const useClerk = await isClerkEnabledForUser(args.request)

  if (useClerk) {
    const { userId } = await getAuth(args)
    return userId ? { provider: 'clerk' as const, id: userId } : null
  }

  const session = await getLegacySession(args.request)
  return session ? { provider: 'legacy' as const, id: session.userId } : null
}
```

Clerk's cookies (`__session`, `__client`) don't conflict with a legacy cookie on a different name. If your legacy app uses `__session`, rename it before the migration starts.

### User data migration

Clerk's [user migration tooling](https://clerk.com/docs/guides/development/migrating/overview.md) covers two official approaches: a one-shot Basic Export/Import using the [open-source migration script](https://github.com/clerk/migration-script) or a Trickle Migration that rehashes insecure passwords to bcrypt on each successful legacy sign-in. Under the hood, both go through the Backend API's [`createUser()`](https://clerk.com/docs/reference/backend/user/create-user.md), which accepts pre-hashed passwords via the `password_digest` + `password_hasher` fields.

Supported hasher values cover most legacy stacks: `bcrypt`, `argon2i`, `argon2id`, `pbkdf2_sha256`, `pbkdf2_sha512`, `scrypt_firebase`, `scrypt_werkzeug`, `awscognito`, `phpass`, and others. Insecure hashers (`md5`, `sha256`, `sha512_symfony`) import successfully and are transparently upgraded to bcrypt on first sign-in.

Three pragmatic strategies:

1. **Basic Export/Import with password hashes**: cleanest if your hashes are bcrypt, argon2, or a supported pbkdf2/scrypt variant with reasonable cost factors.
2. **Force password reset via magic link**: safer if hashes are weak or use an algorithm you'd rather not carry forward.
3. **Trickle migration on first sign-in**: create the Clerk user on first successful legacy sign-in, then cut over. Easiest path if your legacy password stack is non-standard.

## Comparing authentication approaches for Remix

Six common options, one row each. Assume 100K users for the cost column (Clerk uses MRU; other providers use MAU, per the MRU note above).

| Approach                  | Setup  | Cost @ 100K users                   | Prebuilt UI |    MFA   |   SAML/SCIM   | Maintenance |
| ------------------------- | ------ | ----------------------------------- | :---------: | :------: | :-----------: | ----------- |
| DIY (sessions + bcrypt)   | 40–60h | Infra only ($25–50/mo)              |      No     | Build it |       No      | You         |
| `remix-auth` + strategies | 20–30h | Infra only                          |      No     |    No    |       No      | Community   |
| Clerk                     | \~2h   | \~$1,025/mo (Pro + 50K MRU overage) |     Yes     |    Pro   | Pro (metered) | Clerk       |
| Auth0                     | 2–3d   | \~$3,500+/mo                        |     Yes     |    Yes   |      Yes      | Auth0       |
| Supabase Auth             | 1–2d   | \~$188/mo                           |   Archived  |   TOTP   |       No      | Supabase    |
| WorkOS                    | 1d     | Free up to 1M users                 |     Yes     |    Yes   |      Yes      | WorkOS      |

A few notes on the numbers. Clerk's row assumes Pro ($25/mo) plus the $0.02/MRU overage for 50,001–100,000 MRU, which comes out to \~$1,025/mo; rates decline in higher tiers. Auth0's \~$3,500/mo estimate is based on the Professional tier at 100K MAU. Supabase Auth at 100K MAU hits the $25 Pro base plus $0.00325/MAU overage above the 50K free tier (\~$188/mo). WorkOS is free up to 1M users, and you pay for enterprise connections separately.

**When DIY makes sense.** Full data sovereignty is non-negotiable, and you have security expertise in-house.

**When `remix-auth` makes sense.** You want email + password only, no MFA, no hosted UI, and you're okay owning session management.

**When a managed provider makes sense.** The default for most teams. Choose Clerk for React/React Router DX, Supabase if already on Supabase, Auth0/WorkOS if you're selling to enterprise customers with SAML/SCIM from day one.

## Common errors and troubleshooting

Nine real errors you'll hit, with symptoms and fixes.

**1. "clerkMiddleware must be called"**. The middleware future flag is off, or you didn't export middleware from `root.tsx`. Fix: set `future: { v8_middleware: true }` in `react-router.config.ts` and export `const middleware: Route.MiddlewareFunction[] = [clerkMiddleware()]` from `app/root.tsx`.

**2. "useNavigate() may be used only in the context of a Router component"**. You wrapped the Router in `<ClerkProvider>` instead of the other way around. Fix: keep `<ClerkProvider>` inside the default export of `root.tsx`, wrapping `<Outlet />`. React Router's router context must be set up first.

**3. Missing or mismatched environment variables**. You used `CLERK_PUBLISHABLE_KEY` (no prefix) in a Vite project. Fix: rename to `VITE_CLERK_PUBLISHABLE_KEY` for the client-exposed value. `CLERK_SECRET_KEY` stays unprefixed.

**4. `getAuth()` returns `isAuthenticated: false` when you know you're signed in**. Middleware isn't running. Fix: confirm `future: { v8_middleware: true }` in the config and `middleware = [clerkMiddleware()]` in `root.tsx`. Restart the dev server after changing the config.

**5. SSR hydration warnings around auth state**. `<ClerkProvider>` is missing the `loaderData` prop. Fix: export a `loader` from `root.tsx` that returns `rootAuthLoader(args)`, then pass `loaderData` (from `Route.ComponentProps`) to `<ClerkProvider loaderData={loaderData}>`.

**6. Cookie domain / Secure / SameSite issues**. Cookies aren't reaching the app, usually because the production domain isn't registered in Clerk or you're testing over plain HTTP with `Secure` cookies. Fix: for production, add your apex domain in the Clerk dashboard under [**Domains**](https://dashboard.clerk.com/~/domains). For local development with `ngrok` or a tunnel, use the HTTPS URL.

**7. `CLERK_SECRET_KEY` or `CLERK_PUBLISHABLE_KEY` not found on Cloudflare Workers**. The worker entry file is not passing the Cloudflare bindings into React Router's context. `@clerk/react-router` resolves env vars through a fallback chain that checks `context.cloudflare.env` automatically, but only if the request handler is given that shape. Fix: make sure `workers/app.ts` (the Cloudflare Workers entry) forwards `env` as `cloudflare.env`:

```ts
// workers/app.ts (Cloudflare Workers entry)
import { createRequestHandler } from 'react-router'

declare global {
  interface CloudflareEnvironment extends Env {}
}

const requestHandler = createRequestHandler(
  () => import('virtual:react-router/server-build'),
  import.meta.env.MODE,
)

export default {
  async fetch(request, env, ctx) {
    return requestHandler(request, {
      cloudflare: { env, ctx },
    })
  },
} satisfies ExportedHandler<CloudflareEnvironment>
```

With that entry in place, `clerkMiddleware()` called with no arguments resolves both keys from `context.cloudflare.env` — no explicit `publishableKey` / `secretKey` props needed. Set the secrets with `wrangler secret put CLERK_SECRET_KEY` (and the publishable key as a plaintext binding or secret).

**8. Infinite redirect loop on sign-out with React Router v7 middleware**. Documented in [clerk/javascript#5304](https://github.com/clerk/javascript/issues/5304), closed July 2025 pending React Router middleware graduating from unstable. The original reporter's own root-cause analysis attributed the loop to a user-land `requireUserId` helper throwing a `redirect` inside custom middleware. Fix: verify `v8_middleware: true`, verify `clerkMiddleware()` is exported from `root.tsx`, call `signOut({ redirectUrl: '/' })` with an explicit URL, and don't throw redirects from inside your own custom middleware. Do auth checks in loaders instead.

**9. "Invalid future flag: v8\_middleware" on older React Router**. The flag was `unstable_middleware` in React Router v7.3.0–v7.8.x and became `v8_middleware` in v7.9.0 (September 2025). Fix: upgrade to React Router v7.9.0 or later (recommended) and use `future: { v8_middleware: true }`. If you're pinned to an older minor, `future: { unstable_middleware: true }` is the temporary equivalent, but plan to upgrade since `@clerk/react-router` targets the stable flag.

## Deployment considerations

Three platforms cover most React Router apps: Vercel, Cloudflare Workers/Pages, and Node hosts (Fly.io, Railway, Render).

### Vercel

Vercel has native React Router v7 support. Set `VITE_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` in **Project Settings → Environment Variables**, with different values per environment (Production, Preview, Development). Use the default Node.js runtime; Clerk is fully supported there. Production Clerk keys (starting with `pk_live_` / `sk_live_`) won't work with auto-generated preview URLs, so use development keys for previews or set up a staging Clerk instance that accepts your preview domain pattern.

### Cloudflare Workers and Pages

Two specific gotchas.

First, environment variables aren't on `process.env`; they're on the `env` binding provided by Wrangler. The canonical fix is to let the Cloudflare Workers template wire `env` into React Router's context as `context.cloudflare.env` (the `workers/app.ts` entry from the [Cloudflare React Router guide](https://developers.cloudflare.com/workers/framework-guides/web-apps/react-router/) does this by default). Once context is populated, `clerkMiddleware()` called with no arguments resolves `CLERK_SECRET_KEY` and `CLERK_PUBLISHABLE_KEY` from `context.cloudflare.env` automatically — no explicit key-passing required. See the Cloudflare error fix in the troubleshooting section above for the entry-file shape.

Second, DNS records for custom domains need to be in **DNS only** mode (gray cloud in the Cloudflare dashboard), not proxied (orange cloud). Proxying mangles the cookie path and breaks the handshake flow.

### Node hosts: Fly.io, Railway, Render

Docker-based Fly.io uses `fly secrets set CLERK_SECRET_KEY=sk_live_...`. Railway and Render expose env-var UI in their dashboards. Nothing Clerk-specific beyond setting the two keys; Clerk runs on the default Node runtime without adapter shims.

### Production environment checklist

1. `VITE_CLERK_PUBLISHABLE_KEY=pk_live_...` set per-environment
2. `CLERK_SECRET_KEY=sk_live_...` set per-environment
3. `VITE_CLERK_SIGN_IN_URL=/sign-in` and `VITE_CLERK_SIGN_UP_URL=/sign-up`
4. Production domain added to Clerk dashboard under [**Domains**](https://dashboard.clerk.com/~/domains)
5. OAuth providers configured with custom credentials (Clerk's shared dev credentials don't work in production)
6. [Webhook signing secret](https://dashboard.clerk.com/~/webhooks) signing secret stored if using Clerk [webhooks](https://clerk.com/glossary.md#webhook)

## Performance and security best practices

### Session lifetime and rotation

[Clerk's 60-second session JWT, auto-refreshed every 50 seconds](https://clerk.com/docs/guides/how-clerk-works/overview.md) (with a 10-second buffer for network latency), keeps the exploit window for a stolen token narrow. You don't configure this; it's baked into the SDK.

### Minimizing auth round trips

`getAuth()` is cheap: the middleware parses the session once per request and caches the result, so calling `getAuth(args)` from multiple loaders in the same request doesn't re-verify. Calls to the Backend SDK (`clerkClient(args).users.getUser()`) hit Clerk's API, so don't do them in every loader when `sessionClaims` already has what you need.

### Multi-factor authentication

Available on Pro ($25/mo, or $20/mo billed annually). Enable in **Configure → User & authentication → [Multi-factor](https://dashboard.clerk.com/~/user-authentication/multi-factor)**: TOTP (authenticator apps), SMS, and backup codes. `<UserProfile />` exposes the self-service setup automatically; no extra code needed. For step-up auth on sensitive actions (changing an email, deleting an org), use the [`useReverification()`](https://clerk.com/docs/reference/hooks/use-reverification.md) hook.

### Passkeys

Also on Pro, in the same plan after Clerk's February 2026 plan restructure. Enable under **Configure → User & authentication** on the **Passkeys** tab. `<SignIn />` and `<SignUp />` surface passkey enrollment and login automatically. [WebAuthn](https://clerk.com/glossary.md#webauthn) under the hood.

### Bot protection and rate limiting

Clerk automatically rate-limits sign-in attempts and runs bot detection on sign-up. Configurable thresholds in the [Attack protection](https://dashboard.clerk.com/~/protect/attack-protection) section of the dashboard. You don't need a separate rate-limiter in front of the auth routes.

### CSRF protection

Clerk sets `SameSite=Lax` on its cookies. Combined with the 60-second token lifetime, that's sufficient CSRF protection for most state-changing operations. Layer on explicit CSRF tokens only for particularly sensitive flows (think: destructive admin actions on long-lived sessions).

## Implementation checklist

**Setup**

1. [ ] Install `@clerk/react-router`
2. [ ] Set `VITE_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` in `.env.local`
3. [ ] Enable `future: { v8_middleware: true }` in `react-router.config.ts`
4. [ ] Export `middleware = [clerkMiddleware()]` from `app/root.tsx`
5. [ ] Export `loader` that returns `rootAuthLoader(args)` from `app/root.tsx`
6. [ ] Wrap `<Outlet />` with `<ClerkProvider loaderData={loaderData}>`

**Authentication UI**

1. [ ] Create splat route `app/routes/sign-in.tsx` with `<SignIn />`
2. [ ] Create splat route `app/routes/sign-up.tsx` with `<SignUp />`
3. [ ] Register both in `app/routes.ts` as `sign-in/*` and `sign-up/*`
4. [ ] Set `VITE_CLERK_SIGN_IN_URL` and `VITE_CLERK_SIGN_UP_URL`
5. [ ] Add `<UserButton />` to the header
6. [ ] Configure email + OTP in Clerk dashboard
7. [ ] Configure Google and GitHub social connections

**Protection**

1. [ ] Protect loaders with `getAuth()` and `throw redirect('/sign-in')`
2. [ ] Protect actions the same way
3. [ ] Use `<Show>` for client-side UI gates

**Organizations (if using)**

1. [ ] Enable organizations in the Clerk dashboard
2. [ ] Add `<OrganizationSwitcher />` to the header
3. [ ] Gate admin routes with `has({ role: 'org:admin' })`

**Production**

1. [ ] Switch to production Clerk keys (`pk_live_`, `sk_live_`)
2. [ ] Add production OAuth credentials in Clerk dashboard
3. [ ] Add production domain in Clerk dashboard
4. [ ] Verify deployment platform env vars are set

## Frequently Asked Questions

## FAQ

### What is the difference between Remix and React Router v7?

Remix merged into React Router in 2024. What was planned as Remix v3 shipped as React Router v7 in December 2024, and the Remix team now works on React Router v7 as the successor framework. Remix v2 is a maintained legacy path. For new apps, start with React Router v7 in framework mode.

### Can I use Clerk with Remix v2?

Yes, via [`@clerk/remix`](https://www.npmjs.com/package/@clerk/remix). That package is in maintenance mode and receives security updates, not new features. For any new app, use `@clerk/react-router` with React Router v7.

### How do I protect a route in Remix?

In the loader, call `await getAuth(args)` and destructure `isAuthenticated`. If it is false, `throw redirect('/sign-in')` to bounce the visitor to the sign-in page. Actions follow the exact same pattern. The full copy-paste example lives in the "Protecting routes in Remix" section of this article.

### What is the best auth provider for a Remix app?

Clerk is the default for React/Remix DX. Supabase Auth if you are already on Supabase. Auth0 or WorkOS if you need SAML/SCIM from day one. `remix-auth` if you want library-level control and do not need MFA or prebuilt UI.

### How does Clerk handle server-side rendering in Remix?

`clerkMiddleware()` runs before every loader and action, verifying the session JWT. `rootAuthLoader()` returns the current auth state to `<ClerkProvider>` via `loaderData`, which hydrates client state on first paint. Expired tokens trigger a handshake redirect that refreshes the session server-side. Net effect: no loading flicker, no hydration mismatches around auth.

### How do I get the signed-in user in a loader?

Destructure `userId` and `isAuthenticated` from `await getAuth(args)`. For the full User object (email, name, public metadata), pass the ID to `clerkClient(args).users.getUser(userId)` — the Backend SDK returns the full record. The "Working with authentication in loaders and actions" section has the full TypeScript example.

### How do I get the signed-in user in an action?

Exactly the same call as in a loader: `const { isAuthenticated, userId } = await getAuth(args)`. Throw a redirect to `/sign-in` if unauthenticated, then read `await args.request.formData()` for the mutation payload. The "Protecting mutations inside actions" section has the copy-paste example.

### Does Clerk support social login in Remix?

Yes, 30+ providers including Google, GitHub, Apple, Microsoft, Facebook, Discord, LinkedIn, X, GitLab, Slack, and more. Enable them in **Configure → User & authentication → SSO connections** on the **Social** tab. No code change needed; `<SignIn />` and `<SignUp />` render configured providers automatically.

### Does Clerk support email plus one-time code?

Yes. Enable **Email address** as an identifier and **Email verification code** as a verification method in **Configure → User & authentication** on the **Email** tab. Users sign in with an email + 6-digit code; no password required.

### How do I sign users out in Remix?

`<UserButton />` ships with a sign-out item. Programmatically: `const { signOut } = useClerk(); await signOut({ redirectUrl: '/' })`. You can also render `<SignOutButton redirectUrl="/" />` for a simple button.

### Does Clerk issue JWTs I can use for my own API?

Yes. `useAuth().getToken()` returns a 60-second JWT. Verify it server-side with `verifyToken` from `@clerk/backend`. Use [JWT templates](https://clerk.com/docs/guides/sessions/jwt-templates.md) for custom claims or longer-lived tokens for specific integrations.

### How do I migrate from remix-auth to Clerk?

Install `@clerk/react-router`, set the env vars, enable `v8_middleware`, add `clerkMiddleware` and `rootAuthLoader` to `root.tsx`, replace the sign-in form with `<SignIn />`, and swap `authenticator.isAuthenticated()` for `getAuth()` in loaders. Run existing users through [Clerk's user migration tooling](https://clerk.com/docs/guides/development/migrating/overview.md), which accepts bcrypt, argon2i/argon2id, pbkdf2, scrypt variants, and more via the `password_hasher` field on [`createUser()`](https://clerk.com/docs/reference/backend/user/create-user.md).

### How do I add MFA to a Remix app?

Upgrade to Pro ($25/mo, or $20/mo billed annually). In **Dashboard → Multi-factor**, enable TOTP, SMS, and/or backup codes. `<UserProfile />` exposes the self-service setup flow automatically; no code change needed. For step-up auth on sensitive actions, use the [`useReverification()`](https://clerk.com/docs/reference/hooks/use-reverification.md) hook.

### Can I customize Clerk's sign-in UI?

Yes, two ways. For theming and style, pass `appearance` to `<ClerkProvider>` or individual components: `appearance.variables` controls design tokens (colors, fonts, spacing), and `appearance.elements` maps internal element keys to CSS class names. For fully custom markup where Clerk handles the state machine, use the `useSignIn()` and `useSignUp()` hooks from `@clerk/react-router` and build your own form.

### Does Clerk work with Vite and React Router v7 framework mode?

Yes. `@clerk/react-router` targets React Router v7 framework mode on the Vite plugin. Minimum React Router version is 7.1.2; middleware requires React Router v7.9.0+ for the stable `v8_middleware` flag.

### How does Clerk handle session cookies in Remix?

Two cookies. The short-lived `__session` JWT (60 seconds) is readable on your app's domain and is what `getAuth()` reads. The long-lived `__client` cookie (HttpOnly) lives on Clerk's Frontend API domain and is how the auto-refresh works. Both default to `SameSite=Lax` for CSRF protection. Auto-refreshed every 50 seconds via a handshake flow.

### What is the difference between useAuth() and getAuth()?

`useAuth()` is a React hook for client components, returning `userId`, `sessionId`, `sessionClaims`, `has()`, and `getToken()`. `getAuth(args)` is an async server function you call from loaders and actions. Both return the same shape. Use `getAuth()` server-side whenever possible in an SSR app: it avoids a round trip and keeps auth state consistent with the page HTML.

### Is Clerk free to use with Remix?

Yes. Free plan: 50,000 MRU (Monthly Retained Users, meaning users who return 24+ hours after signing up), 100 MROs (organizations) included, 20 members per org, `org:admin` and `org:member` roles, email + OTP, social connections, and bot protection. Pro ($25/mo, or $20/mo billed annually) adds MFA, passkeys, satellite domains, simultaneous sessions, and a 50K MRU base before tiered overage ($0.02/MRU for 50,001–100,000). Custom roles and unlimited members per org require the separate B2B Authentication add-on ($100/mo, or $85/mo billed annually).
