# Clerk

> Clerk AuthStrategy — verifies Clerk-issued __session JWTs via the Frontend API JWKS (no secret key) and maps org_id → tenantId.



---

<!-- source: en/plugins/auth-clerk.md -->
## Clerk

_Clerk AuthStrategy — verifies Clerk-issued __session JWTs via the Frontend API JWKS (no secret key) and maps org_id → tenantId._

`@voltro/plugin-auth-clerk` integrates [Clerk](https://clerk.com): an `AuthStrategy` (conforming to `@voltro/protocol`) that verifies Clerk-issued `__session` JWTs against the **Frontend API JWKS** (public-key verify — no secret key), maps Clerk's Organizations `org_id` to the framework's `tenantId`, and composes with other strategies (your password cookie, an API key, another IdP). It's a thin specialization of the shared `jwtBearerStrategy` — it adds Clerk's defaults and performs no verification of its own. Verified claims arrive on the Subject as `metadata.provider === 'clerk'` + `metadata.claims`.

## Install

```
pnpm add @voltro/plugin-auth-clerk
```

## Wiring

Add `clerkStrategy(...)` to `auth.strategies` in your api's `app.config.ts`. The built-in signed-cookie password strategy always runs first; your strategies run after it, in order, until one `matched`s (or one `failed`s).

```ts
// app.config.ts
import { clerkStrategy } from '@voltro/plugin-auth-clerk'

export default {
  type: 'api' as const,
  name: 'myApi',
  store: 'postgres' as const,
  auth: {
    strategies: [
      clerkStrategy({
        frontendApi: 'https://clerk.yourapp.com',   // required — Clerk Frontend API host
        // audience: '…',                            // ONLY with a Clerk JWT Template
        // defaultTenantId: 'public',                // fallback when no org_id claim
      }),
    ],
  },
}
```

`frontendApi` is the only required option (trailing slash is normalised). From it: JWKS URL defaults to `<frontendApi>/.well-known/jwks.json` and issuer to `<frontendApi>` (Clerk mirrors the host into the `iss` claim). The cookie defaults to `__session` (Clerk's hardcoded session cookie name; pass `null` for header-only).

Tenant resolution maps from `claims.org_id` (Clerk's "Organizations" feature), then `defaultTenantId`. Supply `tenantIdFromClaims(claims)` to derive it (returning `null` is a `failed` verdict). Clerk's default `__session` tokens carry no `aud` claim, so the audience check is off — only set `audience` when you've configured a Clerk **JWT Template** with one.

To accept your own password sessions AND Clerk during a migration, build the chain explicitly with `composeAuthStrategies` (see [auth strategies](/docs/authentication/strategies)):

```ts
import { composeAuthStrategies } from '@voltro/protocol'
import { voltroPasswordStrategy } from '@voltro/plugin-auth'
import { clerkStrategy } from '@voltro/plugin-auth-clerk'

const resolve = composeAuthStrategies([
  voltroPasswordStrategy(),
  clerkStrategy({ frontendApi: process.env.CLERK_FRONTEND_API! }),
])
```

## Options

| Option | Default | Notes |
|---|---|---|
| `frontendApi` | — (**required**) | Clerk Frontend API host. Builds JWKS URL + issuer. |
| `jwksUrl` | `<frontendApi>/.well-known/jwks.json` | Override. |
| `issuer` | `<frontendApi>` | Override. |
| `audience` | (off) | Only with a Clerk JWT Template that sets one. |
| `algorithms` | `['RS256', 'ES256']` | JWT signing-algorithm allowlist; override for a non-default Clerk config. |
| `cookieName` | `'__session'` | Clerk's hardcoded cookie; `null` for header-only. |
| `tenantIdFromClaims` | (uses `org_id`) | Derive `tenantId`; `null` → `failed`. |
| `defaultTenantId` | (none) | Fallback when no `org_id` claim. |
| `scopesFromClaims` | (none) | Map claims (`org_permissions`, or a JWT-Template `permissions`) → `Subject.scopes`. |

Algorithms default to `['RS256', 'ES256']` — pinned (not trusting the token's declared `alg`) to close alg-confusion; override via `algorithms`.

## Environment variables

None read directly — pass `frontendApi` explicitly (typically from your own `defineEnv`-declared vars).

## Security

Server-only; fails closed — a forged, expired, or wrong-audience token, or one with no resolvable tenant, yields `failed`, never `matched`. JWKS public-key verify only; no Clerk secret key is ever handled.

## See also

- [External identity providers](/docs/authentication/external-idp)
- [Auth plugin](/docs/plugins/auth)
