Skip to main content
WorldMonitor uses Clerk for authentication. The auth system gates premium panels behind sign-in and tier checks, and enforces session-based access on server-side API endpoints via local JWT verification.

Auth Stack

LayerTechnologyPurpose
Auth providerClerkSign-in (email, social), session management, hosted UI
JWT verificationjose + Clerk JWKSServer-side bearer token validation (no round-trip)
Convex integrationClerk JWT template (convex)Convex auth with applicationID: "convex"
Auth stateauth-state.tsReactive browser auth state, role caching

Key Files

FilePurpose
convex/auth.config.tsConvex auth provider config — Clerk JWT issuer + applicationID
src/services/clerk.tsClerk instance init, getClerkToken() for Convex JWT template
src/services/auth-state.tsReactive auth state, role fetching, session hydration
src/components/AuthHeaderWidget.tsHeader sign-in button, Clerk UserButton
server/auth-session.tsServer-side JWT validation with jose + cached JWKS

Panel Gating

Premium panels show a CTA overlay instead of content until the user meets the access requirements.

Gate Reasons

ReasonWhat the user seesResolution
ANONYMOUS”Sign In to Unlock”Sign in via Clerk
FREE_TIER”Upgrade to Pro”Upgrade subscription
NONENormal panel contentAlready unlocked

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:
// In FULL_PANELS, FINANCE_PANELS, etc.
'my-panel': { name: 'My Panel', enabled: true, premium: 'locked' }

2. Client-side gate set — src/app/panel-layout.ts

Add the panel key to WEB_PREMIUM_PANELS:
const WEB_PREMIUM_PANELS = new Set([
  'stock-analysis',
  'stock-backtest',
  'daily-market-brief',
  'my-panel',  // <-- add here
]);
This set drives the reactive UI gating — when auth state changes, panels in this set get checked and show/hide CTAs accordingly.

3. Server-side API enforcement (if the panel calls premium APIs)

Client token injectionsrc/services/runtime.ts (WEB_PREMIUM_API_PATHS):
const WEB_PREMIUM_API_PATHS = new Set([
  '/api/market/v1/analyze-stock',
  '/api/market/v1/get-stock-analysis-history',
  '/api/market/v1/backtest-stock',
  '/api/market/v1/list-stored-stock-backtests',
  '/api/my-domain/v1/my-endpoint',  // <-- add here
]);
When a fetch request matches a path in this set and the user has a Clerk session, the client automatically injects Authorization: Bearer <token>. Server gatewayserver/gateway.ts (PREMIUM_RPC_PATHS):
const PREMIUM_RPC_PATHS = new Set([
  '/api/market/v1/analyze-stock',
  '/api/market/v1/get-stock-analysis-history',
  '/api/market/v1/backtest-stock',
  '/api/market/v1/list-stored-stock-backtests',
  '/api/my-domain/v1/my-endpoint',  // <-- add here
]);
The gateway validates the bearer token via local JWKS verification (jose) and checks session.role === 'pro'. Returns 403 if the user isn’t pro.

Currently Gated Panels

PanelVariantsGate type
stock-analysisfull, financelocked (web)
stock-backtestfull, financelocked (web)
daily-market-brieffull, financelocked (web)

Desktop Behavior

Desktop users with a valid WORLDMONITOR_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:
  1. Static API keyX-WorldMonitor-Key header (existing flow, unchanged)
  2. Bearer tokenAuthorization: Bearer <clerk_jwt> (for web users)
The gateway tries the API key first. If that fails on a premium endpoint, it falls back to bearer token validation using local JWKS verification via 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
Non-premium endpoints don’t require any authentication from web origins.

Environment Variables

VariableWherePurpose
CLERK_JWT_ISSUER_DOMAINConvex + VercelClerk issuer domain for JWT verification
VITE_CLERK_PUBLISHABLE_KEYVercelClient-side Clerk publishable key

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.