Auth Stack
| Layer | Technology | Purpose |
|---|---|---|
| Auth provider | Clerk | Sign-in (email, social), session management, hosted UI |
| JWT verification | jose + Clerk JWKS | Server-side bearer token validation (no round-trip) |
| Convex integration | Clerk JWT template (convex) | Convex auth with applicationID: "convex" |
| Auth state | auth-state.ts | Reactive browser auth state, role caching |
Key Files
| File | Purpose |
|---|---|
convex/auth.config.ts | Convex auth provider config — Clerk JWT issuer + applicationID |
src/services/clerk.ts | Clerk instance init, getClerkToken() for Convex JWT template |
src/services/auth-state.ts | Reactive auth state, role fetching, session hydration |
src/components/AuthHeaderWidget.ts | Header sign-in button, Clerk UserButton |
server/auth-session.ts | Server-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
| Reason | What the user sees | Resolution |
|---|---|---|
ANONYMOUS | ”Sign In to Unlock” | Sign in via Clerk |
FREE_TIER | ”Upgrade to Pro” | Upgrade subscription |
NONE | Normal panel content | Already 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:
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
| Panel | Variants | Gate type |
|---|---|---|
stock-analysis | full, finance | locked (web) |
stock-backtest | full, finance | locked (web) |
daily-market-brief | full, finance | locked (web) |
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
Environment Variables
| Variable | Where | Purpose |
|---|---|---|
CLERK_JWT_ISSUER_DOMAIN | Convex + Vercel | Clerk issuer domain for JWT verification |
VITE_CLERK_PUBLISHABLE_KEY | Vercel | Client-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.