Pro monetization β current architecture
Last verified: 2026-07-27 (public lifecycle, plan, price, and capability facts now share one generation chain).
Factual snapshot of how authentication, payments, entitlements, and billing management work today. This page intentionally describes only current deployed behavior.
Stack at a glance
| Concern | Provider | Primary entry points |
|---|---|---|
| Auth | Clerk (@clerk/clerk-js headless on main app, @clerk/clerk-react on /pro) |
src/services/clerk.ts, pro-test/src/services/checkout.ts |
| Payments | Dodo Payments (hosted overlay + full-page return) | convex/lib/dodo.ts, dodopayments-checkout npm SDK |
| Entitlements | Convex (subscriptions + entitlements tables, reactive WebSocket) |
convex/payments/*, src/services/entitlements.ts, src/services/billing.ts |
| Referral attribution | Dodo β Affonso (via metadata.affonso_referral contract) |
convex/payments/checkout.ts:131, convex/payments/subscriptionHelpers.ts:299 |
| Billing portal | Dodo customer portal | api/customer-portal.ts, convex/payments/billing.ts, src/services/billing.ts:openBillingPortal |
| Gateway auth | Clerk bearer JWT + validateBearerToken |
server/auth-session.ts, api/create-checkout.ts |
Tier model
The authoritative lifecycle, plan, price, visibility, and checkout metadata lives in convex/config/productCatalog.ts. The MCP capability count comes from api/mcp/registry/index.ts. npm run product:facts combines those sources into committed Edge, Railway, static, structured-data, and agent-discovery artifacts; normal production build commands run it automatically. npm run product:facts:check is the non-mutating freshness gate.
Products are served at runtime from https://api.worldmonitor.app/api/product-catalog; generated client configuration lives in pro-test/src/generated/tiers.json:
- Free β
price: 0, no productId, card links to dashboard. - Pro Monthly β
pdt_0Nbtt71uObulf7fGXhQup($39.99/mo). - Pro Annual β
pdt_0NbttMIfjLWC10jHQWYgJ($399.99/yr, ~17% discount). - API Starter β
pdt_0NbttVmG1SERrxhygbbUq($99.99/mo, 1k req/day). - API Annual β
pdt_0Nbu2lawHYE3dv2THgSEV($999/yr). - API Business β
pdt_0Nbttg7NuOJrhbyBGCius($299.99/mo, 10k req/day, commercial-use license + 5 bundled Pro seats (same company email domain); monthly-only, published in #4945; StarterβBusiness upgrades ride the Dodo collection/portal path). - Enterprise β
mailto:enterprise@worldmonitor.app(contact sales).
Auth β Clerk
- Init:
src/services/clerk.tsexposesinitClerk(),openSignIn(),signOut(),getClerkToken()(50s cached, in-flight deduped, generation-guarded against account-switch races). - UserButton: mounted by
src/components/AuthHeaderWidget.tswhen signed in;"Sign In"button when signed out. - JWT template:
"convex"preferred (carriesplanclaim for faster server checks); falls back to default session token. /prosurface: lazily loads@clerk/clerk-jsviapro-test/src/services/checkout.ts:ensureClerk(), styled to match marketing page.- Desktop (Tauri): Clerk session token stored in macOS Keychain; deep-link callback wiring under
src-tauri/. - Auth state:
src/services/auth-state.tscentralizes the current session; subscribers include billing watch, entitlement watch, referral service, auth header widget.
Payments β Dodo
Checkout creation
Two Convex actions at convex/payments/checkout.ts:
createCheckout(public action): Convex/Clerk auth.internalCreateCheckout(internal action): called by/relay/create-checkoutwith trusted userId from the edge gateway.
Both share _createCheckoutSession() which:
- Validates
returnUrlagainst an allow-listed set of worldmonitor.app origins. - Builds metadata:
wm_user_id(HMAC-signed viaconvex/lib/identitySigning.ts) + optionalaffonso_referral. - Calls
checkout()fromconvex/lib/dodo.ts. - Returns
{ checkout_url }for overlay open or full-page redirect.
Duplicate guard
Before creating a session, getCheckoutBlockingSubscription checks for active/on_hold/cancelled subs. If one exists, throws/returns ACTIVE_SUBSCRIPTION_EXISTS with the blocking plan info β clients route the user to billing portal instead of creating a second sub.
Overlay vs full-page flow
- Overlay (main app):
src/services/checkout.ts:openCheckout()usesDodoPayments.Checkout.open()withmanualRedirect: true. On success, a sessionStorage flag (wm-post-checkout) is set and the page reloads. Post-reload,consumePostCheckoutFlag()+ entitlement transition detector show the success banner and unlock panels. - Full-page return (fallback /
/propath): Dodo redirects toworldmonitor.app/?subscription_id=...&status=active.src/services/checkout-return.ts:handleCheckoutReturn()reads params, cleans the URL, returns success boolean.
Webhook β subscription lifecycle
convex/payments/subscriptionHelpers.ts handles Dodo webhook events (subscription.active, subscription.renewed, subscription.updated, payment.succeeded, refunds). On first subscription.active, writes subscriptions row, recomputes entitlements, and credits referral attribution if metadata.affonso_referral matches a userReferralCodes row.
Entitlements β Convex
- Schema:
subscriptions(userId, planKey, status, currentPeriodEnd, dodoSubscriptionId) +entitlements(userId, tier, validUntil, derived from subscriptions). - Reactive watch:
src/services/billing.ts:initSubscriptionWatch()subscribes togetSubscriptionForUserover WebSocket. Updates fire within seconds of webhook processing. - Panel gating:
src/services/entitlements.tsexposesisEntitled(),hasTier();panel-layout.tsreloads on freeβpro transition so locked panels unlock without manual refresh. - Cache invalidation: entitlement changes delete the Redis cache entry via Upstash REST API before the reload.
Billing management
- Entry point:
UnifiedSettings.ts:450renders a<button class="manage-billing-btn">Manage Billing</button>inside the settings modal. - Edge gateway:
api/customer-portal.tsvalidates Clerk bearer, relays to/relay/customer-portalon Convex, which calls Dodo to mint a user-scoped portal session. - Client-side:
src/services/billing.ts:openBillingPortal()fetches the portal URL via Convex action and opens in a new tab. Falls back tohttps://customer.dodopayments.com(generic portal) on any failure. - /pro parallel:
pro-test/src/services/checkout.ts:openBillingPortal()is triggered when a/pro-origin checkout hitsACTIVE_SUBSCRIPTION_EXISTS; currently redirects viawindow.location.assign()(same-tab). - Payment failures:
src/components/payment-failure-banner.tsrenders a persistent red banner when subscription status ison_hold; auto-hides on return toactive.
Referral attribution
- Code generation:
/api/referral/me.ts(edge, Clerk-auth'd) returns{ code, shareUrl }wherecodeis a deterministic 8-char HMAC of the Clerk userId usingBRIEF_URL_SIGNING_SECRET. Background binding into Convex viactx.waitUntilβ non-blocking on purpose (see module docstring for rationale). - Share link:
https://worldmonitor.app/pro?ref=<code>. - Attribution point: recipient's checkout metadata carries
affonso_referral: <code>(vendor contract β Dodo β Affonso referral tool; do not rename). On firstsubscription.activewebhook,subscriptionHelpers.ts:299looks up the code inuserReferralCodesand inserts auserReferralCreditsrow crediting the sharer. - Known gap: referral code propagation from the dashboard-origin checkout path is incomplete.
Security & auth surfaces
- Edge endpoints that accept Clerk JWTs must go through
validateBearerToken(server/auth-session.ts). Applies to/api/create-checkout,/api/customer-portal,/api/referral/me. - Middleware UA guard (
middleware.ts): short-UA guard 403s non-browser fetches by default. New API endpoints called from Railway cron must be added toPUBLIC_API_PATHS. - Gateway premium check (
server/gateway.ts): accepts either ClerkpublicMetadata.plan === 'pro'role OR Convexentitlements.tier >= 1 && validUntil >= now. Both signals must agree for a request to be treated as paid. - CORS: Cloudflare Worker
api-cors-preflightis the source of truth forapi.worldmonitor.app. Overridesapi/_cors.js+vercel.json. Worker source lives atworkers/api-cors-preflight/; it short-circuits OPTIONS preflight at the edge (skipping Vercel) and stamps CORS headers onto non-OPTIONS responses on the way back. Unit-tested inworkers/api-cors-preflight/index.test.mjs, smoke-tested live intests/cors-preflight-live.test.mjs(gated byLIVE_SMOKE=1), and deployed by.github/workflows/deploy-worker.ymlon changes underworkers/api-cors-preflight/. The Worker's allowlist + Allow-Headers list MUST stay a superset ofapi/_cors.js#getCorsHeaders; drift breaks credentialed CORS site-wide (2026-05-27 outage post-mortem). - HMAC identity bridge: Dodo metadata
wm_user_idis signed with a server-side key (convex/lib/identitySigning.ts) so webhooks can trust the user association without an additional lookup.
Scope
This public reference documents current deployed behavior. Internal planning and rollout materials are intentionally excluded.
File index (quick reference)
src/services/
βββ clerk.ts # Clerk init + token cache
βββ auth-state.ts # Central auth session
βββ billing.ts # Subscription watch + openBillingPortal
βββ entitlements.ts # Reactive entitlement state
βββ checkout.ts # Dodo overlay orchestration
βββ checkout-return.ts # Post-checkout URL param handling
βββ referral.ts # Share-link fetch + Web Share API
src/components/
βββ AuthHeaderWidget.ts # Signed-in/out header UI
βββ AuthLauncher.ts # Clerk modal launcher
βββ UnifiedSettings.ts # Settings modal (Manage Billing lives here)
βββ payment-failure-banner.ts # on_hold red banner
convex/payments/
βββ checkout.ts # createCheckout + internalCreateCheckout
βββ subscriptionHelpers.ts # Webhook β subscription lifecycle
βββ webhookMutations.ts # Idempotent webhook event processing
βββ billing.ts # getSubscriptionForUser + getCustomerPortalUrl
api/
βββ create-checkout.ts # Edge gateway β Convex relay
βββ customer-portal.ts # Edge gateway β Dodo portal session
βββ referral/me.ts # Clerk-auth'd share-link endpoint
pro-test/src/ # React marketing page
βββ App.tsx # /pro landing
βββ components/PricingSection.tsx
βββ services/checkout.ts # /pro-origin Clerk + Dodo