Auth Stack
Key Files
Panel Gating
Premium panels show a CTA overlay instead of content until the user meets the access requirements.Gate Reasons
How to Configure Which Panels Are Premium
Three files control gating. All three must stay in sync when adding or removing premium panels.1. Panel config — src/config/panels.ts
Add premium: 'locked' to the panel entry in the relevant variant:
2. Client-side gate set — src/app/panel-layout.ts
Add the panel key to WEB_PREMIUM_PANELS:
3. Server-side API enforcement (if the panel calls premium APIs)
Client token injection —src/services/runtime.ts (WEB_PREMIUM_API_PATHS):
Authorization: Bearer <token>.
Server gateway — server/gateway.ts (PREMIUM_RPC_PATHS):
session.role === 'pro'. Returns 403 if the user isn’t pro.
Currently Gated Panels
Desktop Behavior
Desktop users with a validWORLDMONITOR_API_KEY in the Tauri keychain bypass all panel gating. The existing API key flow is unaffected — bearer tokens are a second auth path, not a replacement.
Server-Side Session Enforcement
The Vercel API gateway accepts two forms of authentication for premium endpoints:- Static API key —
X-WorldMonitor-Keyheader (existing flow, unchanged) - Bearer token —
Authorization: Bearer <clerk_jwt>(for web users)
server/auth-session.ts. The JWT is verified against:
- Issuer:
CLERK_JWT_ISSUER_DOMAIN - Audience:
convex(matches the Clerk JWT template) - Signature: RSA256 via Clerk’s published JWKS
Live-session checks for credential issuance
JWT verification is stateless: a token whose Clerk session was revoked (sign-out, “sign out other devices”, an admin revoke) still verifies untilexp. Routes that trade a bearer for a credential that outlives it therefore also ask Clerk whether the token’s session (sid) is still active for the same user — GET /v1/sessions/{sid} via verifyClerkSessionActive (server/auth-session.ts), never cached:
/api/desktop-auth/authorizeverifies the bearer locally, applies its per-user rate limit, then requires a live session before binding a code and stores thesidwith it;/api/desktop-auth/tokenre-checks thatsidafter the PKCE verifier matched and before minting the sign-in ticket./api/user-api-keysverifies the standard session token locally, applies a fail-closed per-user limit (10 per 10 minutes), then requires a live session before issuing an API or desktop key through the gateway-only Convex relay. Key creation is an internal mutation (apiKeys.createApiKeyForUser); there is no public Convex key-creation mutation./api/user-embed-keysapplies the same live-session check and fail-closed per-user limit before issuing an embed key. After body validation, it resolves the Clerk role server-side withlookupClerkPlan(a five-minute cache ofpublic_metadata.plan); an unavailable lookup returns a retryable 503. The gateway-only relay calls the internalembedKeys.createEmbedKeyForUsermutation. There is no public Convex embed-key creation mutation./api/internal/mcp-grant-mintfollows the same order: local verify, per-user rate limit (20 per 10 minutes), then the live-session check, before the nonce or entitlement are read.
sid; JWT-template tokens (including the convex template) get just azp, exp, iat, iss, jti, nbf, sub. The desktop sign-in page, MCP grant page, and API- and embed-key creation services therefore send getClerkSessionToken() (session.getToken() with no template) to these routes, not getClerkToken().
A missing sid or an inactive session is a 401; Clerk being unreachable is a retryable 503 — nothing is issued on a guess. At /token the one-time code is already consumed when the check runs, so its 503 (session_check_unavailable) carries no Retry-After: the desktop restarts the browser sign-in.
Environment Variables
User Roles
User roles (pro / free) are stored as a plan claim in the Clerk JWT. The server extracts this from the verified token payload. Unknown or missing plan values default to free (fail closed — never pro).
On the client side, getAuthState().user?.role exposes the role. Both isProUser() and hasPremiumAccess() check this alongside legacy API key gates.