# 0012: Clerk removed; Convex Auth everywhere

https://exo.now/docs/adr/0012-clerk-removed-convex-auth-everywhere

> Status: accepted · Date: 2026-10-12



# ADR 0012: Clerk removed; Convex Auth everywhere [#adr-0012-clerk-removed-convex-auth-everywhere]

* Status: accepted
* Date: 2026-10-12
* Decided by: Samir (owner)

## Context [#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 [#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 [#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.
