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: 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-clerkWiring
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 matcheds (or one faileds).
// 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):
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.