Decisions and roadmap
ADR 0012: Clerk removed; Convex Auth everywhere
- Status: accepted
- Date: 2026-10-12
- Decided by: Samir (owner)
Context
ADR 0003 moved Exo identity to Convex Auth v2 and ADR 0004 made Exo ID (now Exo Key) the central
identity provider, with every App a relying party. Clerk stayed in EXO as a transitional layer:
the staging gateway at staging.exo.now/{app} used clerkMiddleware and forwarded Clerk session
JWTs to Apps, /sign-in and /sign-up mounted Clerk's <SignIn>/<SignUp>, the repository
contract declared provider: clerk for staging access, and commerce offered a web-clerk-stripe
profile with Clerk Billing. Two identity systems meant two sets of keys, two session models, and
per-MAU pricing for the one we were leaving.
Decision
- Exo ID / Convex Auth is the only identity in EXO. No
@clerk/*package, import, orCLERK_*variable remains; a guard test (tests/int/no-clerk.int.spec.ts) keeps it that way. - EXO signs in through its own Exo ID. EXO is a relying party registered as
exo-staging(convex/lib/exoIdApps.ts) with exact callbackshttps://staging.exo.now/auth/callbackandhttp://localhost:3000/auth/callback./sign-instarts the authorization-code + PKCE round trip through/id/authorize(Google, GitHub, phone over Bird). The access token and rotating refresh token live in host-only HttpOnly cookies. The headlessexoaudience still accepts no browser redirect. - Staging access = Exo ID session + exact non-production Exo grant. The gateway verifies the
Exo ID access token (RS256 against the Exo ID JWKS, exact issuer and
aud: exo-staging, expiry), refreshes it once when expired, then authorizes the requested App path withauthorizeExoAccessagainst theEXO_STAGING_GRANTStester grants. It forwards only the verified access token to the App and strips EXO's own cookies. Every failure stays closed. - Contracts.
project.yaml1.9 requiresaccess.provider: exo-idand forbids Clerk in commerce; 1.8 declarations keep acceptingclerkso existing repositories stay valid while they migrate.convex.application.json1.2 declaresidentity.provider: exo-idwith anaudienceandEXO_ID_ISSUER; 1.1 keeps the Clerk shape. Commerce profileweb-clerk-stripebecomes the Stripe-onlyweb-stripe(Stripe Checkout and Customer Portal own the experience); Stripe and RevenueCat stay. - Data is not broken. The
billingIntegrations.providerliteralclerkstays in the Convex schema, marked deprecated, so imported legacy rows remain valid. Nothing new writes it.
Consequences
- ADR 0002 (Clerk identity boundary) is fully superseded. ADR 0003 and ADR 0004 stand; the
transitional Clerk
/sign-inthat ADR 0004 kept is gone. - The gateway depends on a Convex deploy of the
exo-stagingregistry entry to the EXO deployment, and on theEXO_STAGING_GRANTStester grants being set on the EXO web service. Until both happen, staging apps stay closed (fail closed), not open. - Apps behind the gateway that verified the forwarded Clerk bearer must verify the forwarded
Exo ID access token instead (issuer
${EXO_CONVEX_SITE_URL}/exo-id, audienceexo-staging), or rely on their own Exo ID sign-in. - Repositories outside EXO that still use Clerk (their own
project.yamlor code) are migration debt tracked inconfig/project-integrations.jsonaslegacy identity; EXO no longer provides or documents Clerk. Cancelling the Clerk account and subscription is an owner action.
Source: docs/adr/0012-clerk-removed-convex-auth-everywhere.md