exoDocs
Decisions (ADRs)

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

  1. Exo ID / Convex Auth is the only identity in EXO. No @clerk/* package, import, or CLERK_* variable remains; a guard test (tests/int/no-clerk.int.spec.ts) keeps it that way.
  2. EXO signs in through its own Exo ID. EXO is a relying party registered as exo-staging (convex/lib/exoIdApps.ts) with exact callbacks https://staging.exo.now/auth/callback and http://localhost:3000/auth/callback. /sign-in starts 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 headless exo audience still accepts no browser redirect.
  3. 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 with authorizeExoAccess against the EXO_STAGING_GRANTS tester grants. It forwards only the verified access token to the App and strips EXO's own cookies. Every failure stays closed.
  4. Contracts. project.yaml 1.9 requires access.provider: exo-id and forbids Clerk in commerce; 1.8 declarations keep accepting clerk so existing repositories stay valid while they migrate. convex.application.json 1.2 declares identity.provider: exo-id with an audience and EXO_ID_ISSUER; 1.1 keeps the Clerk shape. Commerce profile web-clerk-stripe becomes the Stripe-only web-stripe (Stripe Checkout and Customer Portal own the experience); Stripe and RevenueCat stay.
  5. Data is not broken. The billingIntegrations.provider literal clerk stays 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-in that ADR 0004 kept is gone.
  • The gateway depends on a Convex deploy of the exo-staging registry entry to the EXO deployment, and on the EXO_STAGING_GRANTS tester 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, audience exo-staging), or rely on their own Exo ID sign-in.
  • Repositories outside EXO that still use Clerk (their own project.yaml or code) are migration debt tracked in config/project-integrations.json as legacy 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

On this page