# Authentication for Remix Applications - Part 2

> Part 2 of 2. Start with [Authentication for Remix Applications](https://clerk.com/articles/authentication-for-remix-applications.md).

This is the second part of our series on adding authentication to a React Router v7 (formerly Remix) application. In Part 1, we covered the core setup, UI integration, and basic route protection. In this part, we dive into advanced topics including organizations and role-based access, migrating an existing app to Clerk, troubleshooting common errors, deployment considerations, and security best practices.

## 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: '/' })` (server) | `useClerk().signOut({ redirectUrl: '/' })` (client) |

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

One shift to plan for: `remix-auth` signs out server-side inside an action, while Clerk's `signOut()` runs on the client — through `<UserButton />`, `<SignOutButton redirectUrl="/" />`, or `useClerk().signOut({ redirectUrl: '/' })`. There is no server-side `signOut()` helper for loaders or actions. To end a session from the server (an admin-initiated force-logout, say), revoke it through the Backend API with `clerkClient(args).sessions.revokeSession(sessionId)`, which signs the user out of the client that session belongs to.

### 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 bills on [MRU](https://clerk.com/glossary.md#monthly-retained-users-mrus) (Monthly Retained Users — users who return 24+ hours after signing up); the other providers bill on MAU (Monthly Active Users), so compare each provider against its own metric.

| 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   | Custom quote                        |     Yes     |    Yes   |      Yes      | Auth0       |
| Supabase Auth             | 1–2d   | \~$25/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 self-serve Professional plan tops out at 20,000 MAU ($3,200/mo); at 100K MAU you're into custom-quoted Enterprise pricing, so there's no published self-serve rate at that volume. Supabase Auth's Pro plan ($25/mo) includes 100,000 MAU, so 100K users sits right at the included ceiling — roughly $25/mo, with per-MAU overage ($0.00325/MAU) only kicking in above 100K. 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 inside `<Show when="signed-in">`
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 inside `<Show when="signed-in">`
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

This concludes our two-part guide on authentication for Remix and React Router v7 applications. You now have a comprehensive understanding of how to implement, secure, and deploy authentication in your React Router apps, from basic setup to advanced organizational features and migration strategies.

## Frequently Asked Questions

## FAQ

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

`<UserButton />` includes a sign-out item. Programmatically: `const { signOut } = useClerk(); await signOut({ redirectUrl: '/' })`. Or render `<SignOutButton redirectUrl="/" />`.

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

Install `@clerk/react-router`, then swap the pieces: `authenticator.isAuthenticated(request)` becomes `getAuth(args)` in loaders and actions; `FormStrategy` and OAuth strategies become `<SignIn />` plus dashboard provider toggles; server-side logout becomes client-side `useClerk().signOut()`. Move existing users with Clerk's [migration tooling](https://clerk.com/docs/guides/development/migrating/overview.md) — a one-shot password-hash import, or a trickle migration that rehashes passwords to bcrypt on each legacy sign-in.

## In this series

1. [Authentication for Remix Applications](https://clerk.com/articles/authentication-for-remix-applications.md)
2. **Authentication for Remix Applications - Part 2** (you are here)
