# World Monitor Pro — Implementation Roadmap ## Context The `/pro` landing page promises features across 4 tiers (Free, Pro, API, Enterprise) but almost nothing beyond the marketing page exists. Current state: - **Convex**: bare — `registrations` + `counters` tables only - **Auth**: none — no Clerk, no sessions. Desktop uses manual `WORLDMONITOR_API_KEY` in keychain - **Payments**: none — no Stripe - **Gating**: UI-only on desktop (6 panels, 3 map layers). No server-side enforcement. `api/_api-key.js` validates against static `WORLDMONITOR_VALID_KEYS` env var - **User dashboard**: none - **API tier**: none (marketed as separate product) - **Delivery channels**: none (Slack/Telegram/Discord/WhatsApp/Email) - **AI briefings**: none (LLM infra exists via Groq but no scheduled briefs) - **Equity research**: basic quotes only — no financials, analyst targets, valuation metrics Key architectural constraint: **main app is vanilla TS + Vite (NOT React)**. Only `pro-test/` landing page is React. Clerk must use `@clerk/clerk-js` headless SDK. **Recommended MVP scope**: Phases 0–4 + tasks 5.1, 5.2 = monetization MVP. Defer Phase 6 XL features until revenue validates demand. --- ## Dependency Graph ``` Phase 0 (Decisions) ├──→ Phase 1 (Auth) ────┐ └──→ Phase 2 (Schema) ──┤ ├──→ Phase 3 (Payments) ──→ Phase 4 (Gating) │ │ │ ┌──────┼──────┐ │ ▼ ▼ ▼ └──────────────────→ Phase 5 Phase 6 Phase 7 (Dashboard) (Pro) (API) │ ▼ Phase 8 (Enterprise) ``` **Critical path**: Decisions → Auth + Schema (parallel) → Payments → Gating → everything else --- ## Summary | Phase | P0 | P1 | P2 | P3 | Total | |-------|----|----|----|----|-------| | 0: Decisions | 3 | — | — | — | 3 | | 1: Auth | 2 | 2 | — | — | 4 | | 2: Schema | 2 | 1 | 1 | — | 4 | | 3: Payments | 3 | 2 | 1 | — | 6 | | 4: Gating | 2 | 2 | — | — | 4 | | 5: Dashboard | — | 2 | 3 | — | 5 | | 6: Pro Features | — | 5 | 3 | — | 8 | | 7: API Tier | — | 2 | 2 | — | 4 | | 8: Enterprise | — | — | — | 10 | 10 | | **Total** | **12** | **16** | **10** | **10** | **48** | --- ## GitHub Issues ### Phase 0: Foundational Decisions --- #### Issue #0.1: Select authentication provider **Title**: `decision: auth provider — Clerk (@clerk/clerk-js headless) vs Convex Auth` **Labels**: `decision`, `auth`, `P0` **Priority**: P0 | **Size**: S | **Dependencies**: None **Description**: Evaluate and select an authentication provider for World Monitor Pro. **Options**: 1. **Clerk** (recommended) — first-class Convex integration, handles email/social login, webhook sync to Convex. `@clerk/clerk-js` headless SDK for vanilla TS app, `@clerk/clerk-react` for `/pro` React page. 2. **Convex Auth** — built-in, fewer moving parts, but newer and less battle-tested. 3. **Supabase Auth** — battle-tested but adds another infra layer on top of Convex. **Key constraint**: Main app is vanilla TS + Vite (NOT React). Auth SDK must support headless DOM mounting (`mountSignIn()` / `mountSignUp()`). **Acceptance criteria**: - [ ] Decision documented with rationale - [ ] Prototype: sign-in flow working in vanilla TS with chosen provider - [ ] Verify Convex webhook sync works (user created in Clerk → user appears in Convex) --- #### Issue #0.2: Select payment provider **Title**: `decision: payment provider — Stripe Checkout (hosted) vs embedded` **Labels**: `decision`, `payments`, `P0` **Priority**: P0 | **Size**: S | **Dependencies**: None **Description**: Select payment processing approach. **Recommendation**: Stripe Checkout (hosted). Simpler than embedded, handles SCA/3DS automatically, less frontend code. Stripe Customer Portal for billing management. **Acceptance criteria**: - [ ] Stripe account configured with test mode - [ ] Decision documented: hosted vs embedded checkout --- #### Issue #0.3: API tier architecture decision **Title**: `decision: API tier architecture — separate Stripe products, independent of Pro plan` **Labels**: `decision`, `api-tier`, `P0` **Priority**: P0 | **Size**: S | **Dependencies**: None **Description**: The marketing page states API is "separate from Pro — use both or either." Define the entitlement model. **Decision points**: - Separate Stripe products: Pro Monthly/Annual + API Starter + API Business - A user can have Pro (dashboard features) without API access, or API access without Pro - Single `entitlements` projection table derives access from all active subscriptions - Rate limits per `rateLimitTier`, not per product **Acceptance criteria**: - [ ] Entitlement matrix documented (which endpoints are free/pro/api-only) - [ ] Schema for `entitlements` projection table designed --- ### Phase 1: Authentication (Weeks 1–2) --- #### Issue #1.1: Clerk + Convex integration **Title**: `feat(auth): Clerk + Convex integration — users table, webhook sync` **Labels**: `auth`, `backend`, `infra`, `P0` **Priority**: P0 | **Size**: M | **Dependencies**: #0.1 **Description**: Set up Clerk as the authentication provider and wire it into Convex via webhook. **Implementation**: 1. Install `@clerk/clerk-js` (vanilla TS main app) + `@clerk/clerk-react` (pro-test React page) 2. Add `users` table to `convex/schema.ts` (see schema in Phase 2) 3. Create Clerk webhook handler as Convex HTTP action: - `user.created` → create user in Convex with `clerkId`, `email`, `name`, `plan: "free"` - `user.updated` → sync email/name changes - `user.deleted` → anonymize/tombstone user records (NOT hard delete audit/billing) 4. Configure environment variables: `VITE_CLERK_PUBLISHABLE_KEY`, `CLERK_SECRET_KEY`, `CLERK_WEBHOOK_SECRET` **Key files**: - `convex/schema.ts` — add users table - `convex/clerk-webhook.ts` — new HTTP action - `.env.example` — add Clerk env vars **Acceptance criteria**: - [ ] User signs up via Clerk → user document created in Convex `users` table - [ ] User updates profile in Clerk → Convex user updated - [ ] Webhook signature verified (reject unsigned/invalid requests) - [ ] Automated: Clerk webhook integration test --- #### Issue #1.2: Sign-in/sign-up UI in vanilla TS dashboard **Title**: `feat(auth): sign-in/sign-up UI in vanilla TS dashboard (clerk-js headless)` **Labels**: `auth`, `frontend`, `P0` **Priority**: P0 | **Size**: M | **Dependencies**: #1.1 **Description**: Add authentication UI to the main vanilla TS dashboard using Clerk's headless `@clerk/clerk-js` SDK. **Implementation**: 1. Initialize `Clerk` instance in app entry point 2. Use `clerk.mountSignIn(element)` / `clerk.mountSignUp(element)` for auth modals 3. Add user avatar + dropdown to existing navbar (sign out, account, billing links) 4. Expose `currentUser` and user entitlements via a service module (`src/services/auth.ts`) 5. Update locked panel CTA from "Join Waitlist" to "Sign Up / Sign In" **Key files**: - `src/main.ts` — Clerk initialization - `src/services/auth.ts` — new auth service - `src/components/Panel.ts` — update locked panel CTA (line ~300) - `src/locales/en.json` — update `premium.joinWaitlist` to "Sign In to Unlock" **Risk**: `@clerk/clerk-js` headless is less documented than React SDK. Prototype `mountSignIn()` early to validate the approach. **Acceptance criteria**: - [ ] Sign in / sign up modal works in vanilla TS app - [ ] User avatar + dropdown in navbar - [ ] Locked panel CTA says "Sign In to Unlock" (or "Upgrade to Pro" if already signed in as free) - [ ] Auth state persists across page refreshes --- #### Issue #1.3: Tauri desktop auth flow **Title**: `feat(auth): Tauri desktop auth flow — PKCE + deep link callback` **Labels**: `auth`, `desktop`, `P1` **Priority**: P1 | **Size**: L | **Dependencies**: #1.1 **Description**: Implement Clerk auth flow for the Tauri desktop app with proper session persistence. **Implementation**: 1. Register `worldmonitor://auth/callback` deep link URI scheme in Tauri config 2. Use PKCE OAuth flow (Clerk supports this) 3. On successful callback, store Clerk session token in macOS Keychain via existing `setSecret()` pattern 4. Token lifecycle: refresh on app foreground, auto-refresh if <5min remaining 5. Logout: clear keychain entry + `clerk.signOut()` + invalidate cached entitlements 6. Fallback: if deep link fails, show one-time code flow (email-based) **Key files**: - `src-tauri/tauri.conf.json` — register deep link - `src-tauri/capabilities/default.json` — add deep-link capability - `src/services/runtime-config.ts` — existing `getSecretState`/`setSecret` **Risk**: Tauri WKWebView has known limitations. Use system browser for OAuth callback, pass token back via deep link. **Acceptance criteria**: - [ ] Sign in works on macOS desktop app - [ ] Session persists across app restarts (keychain) - [ ] Token auto-refreshes - [ ] Sign out clears all cached state --- #### Issue #1.4: Migrate waitlist registrations to users table **Title**: `feat(auth): migrate waitlist registrations → users table` **Labels**: `auth`, `migration`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #1.1 **Description**: Migrate existing Convex `registrations` table entries to the new `users` table. **Migration playbook**: 1. **Dry-run**: migrate to staging Convex first, validate counts match 2. **Dedupe**: normalize emails, merge duplicate registrations by `normalizedEmail` 3. **Consent**: existing Turnstile-verified registrations have implicit consent; send "your account is ready" email with opt-out link via Resend 4. **Create Clerk accounts**: use Clerk Admin API to create user accounts for each registration 5. **Preserve data**: copy `referralCode`, `referralCount`, `source`, `appVersion` 6. **Rollback**: keep `registrations` table intact, only deprecate after 30-day validation period 7. **Validation**: post-migration script compares `registrations` count vs `users` count, flags mismatches **Acceptance criteria**: - [ ] All waitlist emails have corresponding `users` entries - [ ] Referral codes and counts preserved - [ ] "Account ready" emails sent via Resend - [ ] `registrations` table untouched (rollback safety) - [ ] Dry-run report shows 0 mismatches --- ### Phase 2: Convex Schema Expansion (Weeks 1–2, parallel with Phase 1) --- #### Issue #2.1: Core schema — users, subscriptions, entitlements, apiKeys, usage, savedViews, alertRules **Title**: `feat(backend): users/subscriptions/entitlements/apiKeys/usage/savedViews/alertRules schema` **Labels**: `backend`, `convex`, `P0` **Priority**: P0 | **Size**: M | **Dependencies**: #0.1, #0.3 **Description**: Design and implement the full Convex schema for Pro features. **Schema**: ```typescript // New tables alongside existing registrations + counters users: { clerkId: string (indexed), email: string (indexed), name: string, stripeCustomerId?: string (indexed), referralCode: string (indexed), referralCount: number, createdAt: number, updatedAt: number, } subscriptions: { userId: Id<"users"> (indexed), stripeSubscriptionId: string (indexed), product: "pro" | "api_starter" | "api_business", status: "active" | "past_due" | "canceled" | "trialing", currentPeriodStart: number, currentPeriodEnd: number, cancelAtPeriodEnd: boolean, createdAt: number, } entitlements: { userId: Id<"users"> (indexed, unique), dashboardTier: "free" | "pro", apiTier: "none" | "starter" | "business", rateLimitTier: "free_anon" | "free_authed" | "pro" | "api_starter" | "api_business", features: string[], // ["equity_research", "ai_briefs", "saved_views", ...] derivedFrom: Id<"subscriptions">[], computedAt: number, } // Derived projection — recomputed on every subscription change // Single source of truth for ALL gating decisions stripeEvents: { eventId: string (indexed, unique), processedAt: number, eventType: string, } // Idempotency table — prevents duplicate webhook processing apiKeys: { userId: Id<"users"> (indexed), keyHash: string (indexed), // SHA-256 hash — NEVER store plaintext prefix: string, // first 8 chars for UI identification name: string, scopes: string[], // ["read:market", "read:conflict", "*"] tier: "starter" | "business", expiresAt?: number, lastUsedAt?: number, createdAt: number, revokedAt?: number, } // 256-bit random keys (crypto.getRandomValues), prefixed wm_live_ / wm_test_ // Constant-time comparison via crypto.timingSafeEqual on hash usage: { apiKeyId: Id<"apiKeys"> (indexed), date: string, // YYYY-MM-DD endpoint: string, count: number, } savedViews: { userId: Id<"users"> (indexed), name: string, panels: string[], mapLayers: object, watchlistSymbols: string[], createdAt: number, } alertRules: { userId: Id<"users"> (indexed), name: string, type: "threshold" | "event" | "keyword", config: object, channels: object[], enabled: boolean, createdAt: number, } auditLog: { userId: Id<"users"> (indexed), action: string, resource: string, metadata: object, ip?: string, createdAt: number, } // Structured audit for: auth events, key lifecycle, billing changes, entitlement decisions ``` **Key file**: `convex/schema.ts` **Acceptance criteria**: - [ ] All tables created with proper indexes - [ ] Schema passes Convex validation (`npx convex dev`) - [ ] `entitlements` table has unique constraint on `userId` --- #### Issue #2.2: User CRUD mutations & queries **Title**: `feat(backend): user CRUD mutations & queries` **Labels**: `backend`, `convex`, `P0` **Priority**: P0 | **Size**: M | **Dependencies**: #2.1 **Description**: Implement Convex mutations and queries for user management. **Functions**: - `users.getByClerkId(clerkId)` — query - `users.getByApiKey(keyHash)` — query (joins apiKeys → users → entitlements) - `users.create({ clerkId, email, name })` — mutation (from Clerk webhook) - `users.update({ userId, ...fields })` — mutation - `users.anonymize(userId)` — mutation (for account deletion — tombstone PII, preserve audit/billing) - `entitlements.recompute(userId)` — mutation (rebuild from active subscriptions) - `entitlements.getByUserId(userId)` — query - `auditLog.write({ userId, action, resource, metadata, ip })` — mutation **Acceptance criteria**: - [ ] CRUD operations work via Convex dashboard - [ ] `recompute` correctly derives entitlements from multiple subscriptions - [ ] Anonymize replaces PII with `deleted-{hash}` but preserves audit records - [ ] Automated: unit tests for entitlement recomputation (free, pro, api_starter, pro+api_business) --- #### Issue #2.3: API key generation, hashing, and validation **Title**: `feat(backend): API key generation (wm_live_xxx), hashing, validation` **Labels**: `backend`, `convex`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #2.1 **Description**: Implement secure API key lifecycle management. **Implementation**: 1. **Generation**: 256-bit random via `crypto.getRandomValues()`, prefixed `wm_live_` or `wm_test_` 2. **Storage**: SHA-256 hash only in Convex. Plaintext returned once on creation — never again. 3. **Validation**: constant-time comparison via `crypto.timingSafeEqual` on hashed input 4. **Scopes**: per-key permission list (e.g., `["read:market", "read:conflict", "*"]`) 5. **Expiry**: optional `expiresAt` field 6. **Rotation**: create new key → user confirms → revoke old key 7. **Audit**: all create/revoke/rotate events logged to `auditLog` **Functions**: - `apiKeys.create({ userId, name, scopes, tier })` — returns plaintext key ONCE - `apiKeys.validate(keyHash)` — query, returns entitlements if valid - `apiKeys.revoke(keyId)` — mutation, sets `revokedAt` - `apiKeys.listByUser(userId)` — query (returns prefix + metadata, never hash) **Acceptance criteria**: - [ ] Key format: `wm_live_<32 hex chars>` - [ ] Plaintext never stored or logged - [ ] Revoked keys return 401 - [ ] Expired keys return 401 - [ ] Automated: hash/verify round-trip test, constant-time comparison test --- #### Issue #2.4: Usage tracking — daily counters **Title**: `feat(backend): usage tracking — daily counters per API key per endpoint` **Labels**: `backend`, `convex`, `P2` **Priority**: P2 | **Size**: S | **Dependencies**: #2.1 **Description**: Track API usage per key per day for billing and dashboard display. **Functions**: - `usage.record(apiKeyId, endpoint)` — mutation (increment or create daily counter) - `usage.getDaily(apiKeyId, date)` — query - `usage.getMonthly(apiKeyId, month)` — query (aggregate) **Acceptance criteria**: - [ ] Daily counters increment correctly - [ ] Monthly aggregation sums daily values --- ### Phase 3: Payments — Stripe (Weeks 3–4) --- #### Issue #3.1: Stripe products and prices **Title**: `feat(payments): Stripe products — Pro Monthly/Annual + API Starter/Business` **Labels**: `payments`, `infra`, `P0` **Priority**: P0 | **Size**: S | **Dependencies**: #0.2 **Description**: Create Stripe products and price objects for all tiers. **Products**: 1. **World Monitor Pro Monthly** — $X/mo 2. **World Monitor Pro Annual** — $X/yr (discount) 3. **World Monitor API Starter** — $Y/mo (1,000 req/day, 5 webhook rules) 4. **World Monitor API Business** — $Z/mo (50,000 req/day, unlimited webhooks + SLA) **Environment variables**: - `STRIPE_SECRET_KEY`, `STRIPE_PUBLISHABLE_KEY`, `STRIPE_WEBHOOK_SECRET` - `STRIPE_PRO_MONTHLY_PRICE_ID`, `STRIPE_PRO_ANNUAL_PRICE_ID` - `STRIPE_API_STARTER_PRICE_ID`, `STRIPE_API_BUSINESS_PRICE_ID` **Acceptance criteria**: - [ ] Products created in Stripe test mode - [ ] Price IDs stored as env vars - [ ] `.env.example` updated --- #### Issue #3.2: Checkout flow via Convex HTTP action **Title**: `feat(payments): checkout flow — Convex HTTP action → Stripe Checkout redirect` **Labels**: `payments`, `backend`, `P0` **Priority**: P0 | **Size**: M | **Dependencies**: #2.1, #3.1 **Description**: Create a Convex HTTP action that generates a Stripe Checkout Session and returns the URL. **Implementation**: 1. Convex HTTP action receives authenticated user ID + product choice 2. Look up or create Stripe customer (store `stripeCustomerId` on user) 3. Create Stripe Checkout Session with `success_url` and `cancel_url` 4. Return checkout URL for client-side redirect 5. Handle upgrade (free → pro), API tier purchase, and plan switching **Acceptance criteria**: - [ ] Authenticated user can initiate checkout - [ ] Redirects to Stripe Checkout - [ ] Success URL leads back to dashboard with success message - [ ] Stripe customer ID stored on user record --- #### Issue #3.3: Stripe webhook handler **Title**: `feat(payments): Stripe webhook handler — subscription lifecycle in Convex` **Labels**: `payments`, `backend`, `P0` **Priority**: P0 | **Size**: L | **Dependencies**: #3.2 **Description**: Handle Stripe webhook events to manage subscription lifecycle in Convex. **Safety requirements**: - **Signature verification**: `stripe.webhooks.constructEvent()` with `STRIPE_WEBHOOK_SECRET` - **Idempotency**: check `stripeEvents` table by `event.id` before processing; skip duplicates - **Event age monitoring**: log alerts for events older than 5 minutes (indicates outage/retry), but do NOT reject them — legitimate Stripe retries can arrive late - **Subscription reconciliation**: do NOT use a forward-only state machine. Fetch current subscription object via `stripe.subscriptions.retrieve()` and reconcile `status`, `current_period_end`, and `items`. This correctly handles `past_due → active`, resumed subscriptions, and plan switches. - **Dead-letter**: failed processing logged to `auditLog` with full event payload for manual retry - **Entitlement recomputation**: every subscription change triggers `recomputeEntitlements(userId)` + Redis cache invalidation **Webhook events**: - `checkout.session.completed` → create subscription, link to user, recompute entitlements - `invoice.paid` → renew subscription period - `invoice.payment_failed` → update status to `past_due`, send warning email via Resend - `customer.subscription.updated` → reconcile from Stripe object, recompute entitlements - `customer.subscription.deleted` → mark canceled, recompute entitlements (downgrade) **Acceptance criteria**: - [ ] All 5 webhook events handled correctly - [ ] Duplicate events are idempotent (no double processing) - [ ] Entitlements update within seconds of payment - [ ] Failed webhooks logged for manual retry - [ ] Automated: webhook contract tests via Stripe CLI `trigger` --- #### Issue #3.4: Pricing page **Title**: `feat(payments): pricing page — replace waitlist form with real plans + checkout` **Labels**: `payments`, `frontend`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #3.2 **Description**: Replace the current waitlist form on `/pro` with a real pricing page that initiates checkout. **Implementation**: - Side-by-side comparison: Free vs Pro vs API vs Enterprise - Monthly/annual toggle for Pro - "Get Started" buttons → Clerk sign-in (if not authed) → Stripe Checkout - "Coming Soon" section for Enterprise with "Contact Sales" CTA - Integrate with existing i18n (23 languages) **Acceptance criteria**: - [ ] Pricing page shows all tiers with features - [ ] Checkout flow works end-to-end - [ ] Works in all 23 supported languages --- #### Issue #3.5: Billing management via Stripe Customer Portal **Title**: `feat(payments): billing management via Stripe Customer Portal` **Labels**: `payments`, `frontend`, `P1` **Priority**: P1 | **Size**: S | **Dependencies**: #3.3 **Description**: Add a link/button that redirects to Stripe Customer Portal for self-service billing management (update payment method, view invoices, cancel subscription, switch plans). **Acceptance criteria**: - [ ] Portal link accessible from `/account/billing` - [ ] User can update payment method, view invoices, cancel --- #### Issue #3.6: 14-day free trial for Pro **Title**: `feat(payments): 14-day free trial for Pro` **Labels**: `payments`, `backend`, `P2` **Priority**: P2 | **Size**: S | **Dependencies**: #3.3 **Description**: Configure Stripe to offer a 14-day trial for Pro tier (no credit card required). Trial expiry → email reminder via Resend. Auto-downgrade on trial end via webhook. **Acceptance criteria**: - [ ] Trial activates without credit card - [ ] Reminder email sent 3 days before trial ends - [ ] Auto-downgrade on expiry triggers entitlement recomputation --- ### Phase 4: Feature Gating (Week 5) --- #### Issue #4.1: Server-side entitlement verification in gateway.ts **Title**: `feat(gating): server-side entitlement verification in gateway.ts` **Labels**: `gating`, `backend`, `P0` **Priority**: P0 | **Size**: L | **Dependencies**: #2.2, #2.3 **Description**: Add entitlement-aware middleware to the server gateway so pro-only endpoints are enforced server-side. **Implementation**: 1. After `validateApiKey()` (gateway.ts line 161), inject entitlement check 2. Look up user entitlements from Redis cache (`ent:{userId}` or `ent:key:{keyHash}`) 3. If cache miss, query Convex, populate cache with 60s TTL 4. **Active invalidation**: `recomputeEntitlements()` deletes Redis cache entry immediately via Upstash REST API 5. **Fail-closed**: if Redis AND Convex both unavailable, return 503 (never grant unauthorized access) 6. Check endpoint against entitlement matrix 7. Return `403 { error: "Upgrade required", requiredPlan: "pro", upgradeUrl: "/pro" }` for gated endpoints **Entitlement matrix**: | Endpoint Category | free_anon | free_authed | pro | api_starter | api_business | |---|---|---|---|---|---| | Public data (seismology, news, weather) | ✓ | ✓ | ✓ | ✓ | ✓ | | Market quotes, crypto, commodities | ✓ | ✓ | ✓ | ✓ | ✓ | | Equity research (financials, targets) | — | — | ✓ | — | ✓ | | AI briefs, flash alerts | — | — | ✓ | — | — | | Economy analytics (correlations) | — | — | ✓ | — | ✓ | | Risk scoring, scenario analysis | — | — | ✓ | — | ✓ | **API key migration (dual-read rollout)**: 1. **Phase A**: validate against BOTH static `WORLDMONITOR_VALID_KEYS` AND new entitlements. Log comparison metrics. 2. **Phase B**: after 1 week with 0 mismatches, flip flag to new system only. Keep env var as emergency rollback. 3. **Phase C**: remove static key validation code after 30 days. **Key files**: - `server/gateway.ts` — main middleware injection point - `api/_api-key.js` — extend validation logic - `server/_shared/rate-limit.ts` — rate limit by entitlement tier **Acceptance criteria**: - [ ] Free user gets 403 on equity research endpoint - [ ] Pro user gets 200 on equity research endpoint - [ ] API starter gets 200 on data endpoints, 403 on dashboard-only features - [ ] Fail-closed: 503 when Redis + Convex both down - [ ] Dual-read metrics dashboard shows match/mismatch counts - [ ] Automated: E2E entitlement gating tests per tier --- #### Issue #4.2: Client-side plan context service **Title**: `feat(gating): client-side plan context service (src/services/plan-context.ts)` **Labels**: `gating`, `frontend`, `P0` **Priority**: P0 | **Size**: M | **Dependencies**: #1.2, #2.2 **Description**: Create a client-side service that exposes user plan/entitlements for UI gating. **Implementation**: 1. New service: `src/services/plan-context.ts` 2. On auth, query user entitlements from Convex 3. Expose helpers: `isPro()`, `hasApiAccess()`, `getPlan()`, `hasFeature(name)` 4. Replace ALL `getSecretState('WORLDMONITOR_API_KEY').present` checks with plan context 5. Works for both web (Clerk session) and desktop (Clerk + Tauri keychain) 6. Include `computedAt` timestamp for staleness detection **Key files to update**: - `src/components/Panel.ts` — replace `getSecretState` check - `src/components/DeckGLMap.ts` — replace layer premium check - `src/components/GlobeMap.ts` — replace layer premium check - `src/app/panel-layout.ts` — replace `_wmKeyPresent` logic **Acceptance criteria**: - [ ] `isPro()` returns true for pro users, false for free - [ ] All `getSecretState('WORLDMONITOR_API_KEY')` references replaced - [ ] Plan context updates within 60s of subscription change --- #### Issue #4.3: Refactor panel/layer premium flags **Title**: `feat(gating): refactor panel/layer premium flags → plan context` **Labels**: `gating`, `frontend`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #4.2 **Description**: Update panel and map layer configurations to use the new plan context instead of desktop-only `isDesktopRuntime()` checks. **Changes**: - `src/config/panels.ts` — premium flags read from plan context, apply to web AND desktop - `src/config/map-layer-definitions.ts` — same - Locked panel CTA: "Upgrade to Pro" → links to pricing page (not waitlist) - Expand locked panels beyond current 2+4 to cover all pro-tier features **Acceptance criteria**: - [ ] Premium gating works on web (not just desktop) - [ ] Locked panels link to `/pro` pricing page - [ ] Enhanced panels show "PRO" badge for free users --- #### Issue #4.4: Per-plan rate limiting **Title**: `feat(gating): per-plan rate limiting in gateway` **Labels**: `gating`, `backend`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #4.1 **Description**: Implement tiered rate limiting based on user plan. **Rate limits**: | Tier | Requests/day | Requests/min | |------|-------------|-------------| | Free (no auth) | 100 | 5 | | Free (authenticated) | 500 | 10 | | Pro | 10,000 | 60 | | API Starter | 1,000 | 30 | | API Business | 50,000 | 300 | **Unauthenticated identity**: - Key: `cf-connecting-ip` + endpoint path bucket - **Trusted-proxy rule**: only honor `cf-connecting-ip` from Cloudflare IP ranges. Non-CF sources fall back to actual remote address. Log spoofing attempts. - Turnstile challenge at 50% daily quota - Abuse flag at 3x daily limit **Acceptance criteria**: - [ ] Free user rate-limited at 100 req/day - [ ] Pro user rate-limited at 10,000 req/day - [ ] Rate limit headers returned: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` - [ ] Automated: rate limit integration tests per tier --- ### Phase 5: User Dashboard (Weeks 6–7) --- #### Issue #5.1: Account page **Title**: `feat(dashboard): /account page — profile, plan badge, API keys` **Labels**: `dashboard`, `frontend`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #1.2, #2.2, #2.3 **Description**: Create an account page at `/account` showing profile info, current plan, and API key management. **Sections**: 1. **Profile**: name, email (from Clerk), avatar 2. **Plan**: current plan badge, expiry date, upgrade button 3. **API Keys**: list keys (prefix only), create, copy (once), revoke, regenerate 4. **Referral**: referral code, count, share link (preserved from waitlist) **Acceptance criteria**: - [ ] Profile info displayed from Clerk - [ ] Plan badge shows current tier - [ ] API key CRUD works (create shows plaintext once, subsequent views show prefix only) - [ ] Referral stats visible --- #### Issue #5.2: Billing page **Title**: `feat(dashboard): /account/billing — invoices, plan management` **Labels**: `dashboard`, `frontend`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #3.3 **Description**: Create a billing page showing current subscription, next payment date, and link to Stripe Customer Portal. **Sections**: 1. Current plan + next billing date + amount 2. Payment method (last 4 digits) 3. Link to Stripe Customer Portal for self-service management 4. Upgrade/downgrade buttons **Acceptance criteria**: - [ ] Shows current plan and billing cycle - [ ] Stripe portal link works - [ ] Upgrade/downgrade triggers new checkout --- #### Issue #5.3: API usage stats **Title**: `feat(dashboard): API usage stats with daily/monthly charts` **Labels**: `dashboard`, `frontend`, `P2` **Priority**: P2 | **Size**: M | **Dependencies**: #2.4, #5.1 **Description**: Display API usage charts on the account page (daily and monthly breakdowns). **Acceptance criteria**: - [ ] Daily usage bar chart - [ ] Monthly aggregation - [ ] Per-endpoint breakdown --- #### Issue #5.4: Notification preferences & delivery channels **Title**: `feat(dashboard): notification preferences & delivery channels config` **Labels**: `dashboard`, `frontend`, `P2` **Priority**: P2 | **Size**: M | **Dependencies**: #5.1 **Description**: Settings page for configuring notification delivery channels (Slack webhook URL, Telegram bot, Discord webhook, email preferences). **Acceptance criteria**: - [ ] Add/remove delivery channels - [ ] Test notification button per channel - [ ] Channel credentials encrypted at rest --- #### Issue #5.5: API documentation page **Title**: `feat(docs): interactive API docs page with OpenAPI 3.1 from proto defs` **Labels**: `dashboard`, `docs`, `P2` **Priority**: P2 | **Size**: L | **Dependencies**: #4.1 **Description**: Generate OpenAPI 3.1 spec from existing sebuf proto definitions (50+ RPCs across 15+ domains). Host interactive API explorer. **Implementation**: - Parse proto files with `(sebuf.http.config)` annotations - Generate OpenAPI 3.1 spec - Host via Swagger UI or Redoc at `/developers` or `/api/docs` - Per-endpoint plan requirements documented - Code examples in curl, Python, JS/TS **Acceptance criteria**: - [ ] OpenAPI spec covers all public endpoints - [ ] Interactive explorer works - [ ] Plan requirements shown per endpoint --- ### Phase 6: Pro Features (Weeks 8–12) --- #### Issue #6.1: Equity research data pipeline **Title**: `feat(pro): equity research — financials, analyst targets, valuation metrics` **Labels**: `pro-feature`, `backend`, `P1` **Priority**: P1 | **Size**: XL | **Dependencies**: #4.1 **Description**: Build a new data pipeline for equity research features (pro-only). **Data to add**: - Financial statements (income, balance sheet, cash flow) - Analyst consensus price targets - Valuation metrics (PE, PB, EV/EBITDA) - Earnings calendar **Sources** (evaluate): Finnhub premium, Financial Modeling Prep (FMP), Alpha Vantage **Implementation**: - New sebuf proto service: `worldmonitor/equity/v1/` - RPCs: `get-company-financials`, `get-analyst-consensus`, `get-valuation-metrics`, `list-earnings-calendar` - Redis caching via `cachedFetchJson` pattern - New panel: Equity Research dashboard (pro-only, gated via entitlements) **Acceptance criteria**: - [ ] Financial data available for major US stocks - [ ] Analyst targets displayed with consensus rating - [ ] Equity panel shows for pro users, locked for free - [ ] Data refreshes at least daily --- #### Issue #6.2: AI daily briefs engine **Title**: `feat(pro): AI daily briefs engine — scheduled LLM summaries` **Labels**: `pro-feature`, `backend`, `P1` **Priority**: P1 | **Size**: XL | **Dependencies**: #4.1, #6.5 **Description**: Build a scheduled AI briefing system that synthesizes overnight developments and delivers via configured channels. **Implementation**: 1. Cron job (Railway or Convex cron) runs at configurable time per user timezone 2. Aggregates latest data from Redis bootstrap keys (40+ keys exist) 3. Ranks events by user's configured focus areas (markets, geopolitics, energy, etc.) 4. Generates structured brief via Groq LLM (infrastructure exists in `deduct-situation.ts`) 5. Stores brief in Convex `briefs` table 6. Delivers via configured channels (email, Slack, Telegram, Discord) **Flash alerts**: real-time event detection → LLM classification (existing `classify-event` RPC) → push notification **Acceptance criteria**: - [ ] Daily brief generated and delivered - [ ] Focus areas configurable per user - [ ] Brief stored and viewable in dashboard - [ ] Flash alerts delivered within 5 minutes of event --- #### Issue #6.3: Sub-60s data refresh **Title**: `feat(pro): sub-60s data refresh for pro users` **Labels**: `pro-feature`, `backend`, `P1` **Priority**: P1 | **Size**: L | **Dependencies**: #4.1 **Description**: Reduce data refresh interval for pro users from 5-15 minutes to <60 seconds. **Phased approach**: 1. **Phase 1**: reduce client-side polling interval based on plan (simplest — just change `DataLoaderManager` interval for pro users) 2. **Phase 2**: Server-Sent Events (SSE) for high-frequency data (markets, alerts) — push new data as it arrives **Acceptance criteria**: - [ ] Pro users see data refresh <60s - [ ] Free users unchanged (5-15 min) - [ ] Server load monitored (10x more requests from pro) --- #### Issue #6.4: Server-side watchlists & custom views **Title**: `feat(pro): persistent server-side watchlists & custom views` **Labels**: `pro-feature`, `fullstack`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #2.1, #4.2 **Description**: Migrate watchlists from localStorage to Convex `savedViews` table for cross-device sync. **Currently localStorage-only**: - `src/services/market-watchlist.ts` - `src/services/aviation/watchlist.ts` **Implementation**: - On first sign-in, import existing localStorage watchlists to Convex - Sync changes bidirectionally (Convex → client on load, client → Convex on change) - Cross-device sync (web ↔ desktop) **Acceptance criteria**: - [ ] Watchlists persist across devices - [ ] localStorage data migrated on first sign-in - [ ] Offline-first: works without connection, syncs on reconnect --- #### Issue #6.5: Delivery channels — Slack, Telegram, Discord, Email **Title**: `feat(pro): delivery channels — Slack, Telegram, Discord, Email` **Labels**: `pro-feature`, `backend`, `P1` **Priority**: P1 | **Size**: XL | **Dependencies**: #2.1 **Description**: Build multi-channel delivery infrastructure for AI briefs and alerts. **Channels** (in priority order): 1. **Email** — Resend (already integrated). Extend for formatted briefs/alerts. 2. **Slack** — incoming webhook URL (user provides). Format messages with blocks. 3. **Telegram** — Bot API. Create `@WorldMonitorBot`. User starts conversation, store `chat_id`. 4. **Discord** — webhook URL (user provides). Format with embeds. 5. **WhatsApp** — P3 (requires Twilio/Meta business verification, highest cost) **Security**: - Webhook URL allowlisting: only `hooks.slack.com`, `discord.com/api/webhooks`, Telegram API - Secrets encrypted via server-managed envelope encryption (`APP_ENCRYPTION_KEY` env var) - PII redacted from outbound payloads - Per-channel signing/verification where supported **Acceptance criteria**: - [ ] Email delivery works (formatted brief) - [ ] Slack webhook delivery works - [ ] Telegram bot delivery works - [ ] Discord webhook delivery works - [ ] Secrets encrypted at rest - [ ] Test notification button per channel --- #### Issue #6.6: Economy analytics — correlation views **Title**: `feat(pro): economy analytics — GDP/inflation/rates correlation views` **Labels**: `pro-feature`, `frontend`, `P2` **Priority**: P2 | **Size**: L | **Dependencies**: #4.1 **Description**: Build correlation views on top of existing economic data (FRED, BIS, World Bank RPCs already exist). **New visualizations**: - GDP growth vs market performance - Inflation trends vs central bank rates - Growth cycle detection and labeling - Cross-country comparison charts **Acceptance criteria**: - [ ] Correlation charts display correctly - [ ] Data from existing FRED/BIS/World Bank endpoints - [ ] Pro-only (gated via entitlements) --- #### Issue #6.7: Risk monitoring — convergence alerting & scenario analysis **Title**: `feat(pro): risk monitoring — convergence alerting & scenario analysis` **Labels**: `pro-feature`, `fullstack`, `P2` **Priority**: P2 | **Size**: L | **Dependencies**: #4.1 **Description**: Enhance existing risk analytics with scenario analysis and convergence alerting. **Existing engines** (enhance, don't rebuild): - `src/services/geo-convergence.ts` — convergence detection - `src/services/focal-point-detector.ts` — focal point detection - `src/services/country-instability.ts` — CII scoring - `src/services/signal-aggregator.ts` — signal aggregation **New**: - Scenario analysis UI (what-if modeling) - Convergence alerting (push when signals converge in a region) - Risk trend visualization over time **Acceptance criteria**: - [ ] Scenario analysis UI works - [ ] Convergence alerts delivered via configured channels - [ ] Pro-only (gated via entitlements) --- #### Issue #6.8: 22-services-1-key **Title**: `feat(pro): 22-services-1-key — replace BYOK with Pro key for all services` **Labels**: `pro-feature`, `fullstack`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #4.1 **Description**: Pro users should NOT need to configure individual API keys for Finnhub, FRED, ACLED, etc. A single World Monitor Pro subscription gives access to all 24 services. **Implementation**: - Server-side: pro requests use World Monitor's own upstream API keys (already configured as env vars) - Free tier: continues using BYOK via desktop settings panel - Gateway identifies pro user → skips BYOK requirement → uses server-side keys for upstream calls **Key files**: - `src/services/settings-constants.ts` — 20+ key definitions - `server/gateway.ts` — skip BYOK check for pro users **Acceptance criteria**: - [ ] Pro user sees data without configuring any individual API keys - [ ] Free user still uses BYOK - [ ] No upstream API key leakage to client --- ### Phase 7: API Tier (Weeks 10–14, separate product) --- #### Issue #7.1: API key issuance & management portal **Title**: `feat(api-tier): API key issuance & management portal` **Labels**: `api-tier`, `fullstack`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #2.3, #5.1 **Description**: Extend the account page with API key management specifically for API tier subscribers. **Features**: - Create multiple keys with different names/scopes - View usage per key - Rotate keys (create new → confirm → revoke old) - Set per-key rate limits within tier allowance **Acceptance criteria**: - [ ] Multiple keys can be created - [ ] Per-key usage visible - [ ] Key rotation flow works --- #### Issue #7.2: Per-key usage tracking with daily limits **Title**: `feat(api-tier): per-key usage tracking with daily limits` **Labels**: `api-tier`, `backend`, `P1` **Priority**: P1 | **Size**: M | **Dependencies**: #2.4, #4.4 **Description**: Track and enforce daily usage limits per API key based on tier (Starter: 1,000/day, Business: 50,000/day). **Implementation**: - Increment `usage` counter on each request - Check daily total before processing - Return `429` with `Retry-After` header when limit exceeded - Dashboard shows usage vs limit **Acceptance criteria**: - [ ] Daily limit enforced per key - [ ] 429 returned with retry info when exceeded - [ ] Usage dashboard shows consumption --- #### Issue #7.3: OpenAPI 3.1 spec from sebuf protos **Title**: `feat(api-tier): OpenAPI 3.1 spec auto-generation from sebuf protos` **Labels**: `api-tier`, `docs`, `P2` **Priority**: P2 | **Size**: L | **Dependencies**: None **Description**: Auto-generate OpenAPI 3.1 specification from existing sebuf proto definitions. **Acceptance criteria**: - [ ] Spec covers all public RPC endpoints - [ ] Plan requirements documented per endpoint - [ ] Code examples in curl, Python, JS/TS --- #### Issue #7.4: Webhook delivery system **Title**: `feat(api-tier): webhook delivery system with retry & HMAC signatures` **Labels**: `api-tier`, `backend`, `P2` **Priority**: P2 | **Size**: L | **Dependencies**: #7.1 **Description**: Allow API tier subscribers to configure webhook endpoints for event delivery. **Implementation**: - Convex table: `webhookEndpoints` (userId, url, events, secret) - HMAC-SHA256 signature on each delivery - Exponential backoff retry (3 attempts) - Starter: 5 webhook rules; Business: unlimited - Delivery log with status codes **Acceptance criteria**: - [ ] Webhook delivery works with signature - [ ] Failed deliveries retried with backoff - [ ] Tier limits enforced --- ### Phase 8: Enterprise (Months 4–12+, all P3) --- #### Issue #8.1: Organization accounts & team management **Title**: `feat(enterprise): organization accounts & team management` **Labels**: `enterprise`, `fullstack`, `P3` **Priority**: P3 | **Size**: XL | **Dependencies**: #1.1 **Description**: Multi-user organizations with shared dashboards, seat management, and invite flow. --- #### Issue #8.2: RBAC **Title**: `feat(enterprise): RBAC (role-based access control)` **Labels**: `enterprise`, `backend`, `P3` **Priority**: P3 | **Size**: L | **Dependencies**: #8.1 **Description**: Role-based access: admin, analyst, viewer. Per-role permissions for panels, data access, and configuration. --- #### Issue #8.3: SSO (SAML/OIDC) **Title**: `feat(enterprise): SSO (SAML/OIDC via Clerk Enterprise)` **Labels**: `enterprise`, `auth`, `P3` **Priority**: P3 | **Size**: L | **Dependencies**: #8.1 **Description**: Enterprise SSO via Clerk's Enterprise plan. Requires Clerk Enterprise subscription. --- #### Issue #8.4: TV/SOC display mode **Title**: `feat(enterprise): TV/SOC display mode` **Labels**: `enterprise`, `frontend`, `P3` **Priority**: P3 | **Size**: M | **Dependencies**: None **Description**: Full-screen dashboard for wall displays with auto-rotating panels and custom layouts. Some exists in `src/services/tv-mode.ts`. --- #### Issue #8.5: White-label & embeddable panels **Title**: `feat(enterprise): white-label & embeddable panels` **Labels**: `enterprise`, `frontend`, `P3` **Priority**: P3 | **Size**: XL **Description**: Your brand, your domain, your desktop app. Embeddable iframe panels (50+ available). --- #### Issue #8.6: Satellite imagery & SAR integration **Title**: `feat(enterprise): satellite imagery & SAR integration` **Labels**: `enterprise`, `backend`, `P3` **Priority**: P3 | **Size**: XL **Description**: Live-edge satellite imagery and SAR (Synthetic Aperture Radar) with change detection. Requires partnerships with Maxar/Planet. --- #### Issue #8.7: AI agents with investor personas & MCP **Title**: `feat(enterprise): AI agents with investor personas & MCP` **Labels**: `enterprise`, `backend`, `P3` **Priority**: P3 | **Size**: XL **Description**: Autonomous intelligence agents using Model Context Protocol. Connect as tool to Claude, GPT, or custom LLMs. --- #### Issue #8.8: 100+ data connectors **Title**: `feat(enterprise): 100+ data connectors` **Labels**: `enterprise`, `backend`, `P3` **Priority**: P3 | **Size**: XL **Description**: PostgreSQL, Snowflake, Splunk, Sentinel, Jira, Slack, Teams. Export to PDF, PowerPoint, CSV, GeoJSON. Multi-quarter effort. --- #### Issue #8.9: On-premises / air-gapped deployment **Title**: `feat(enterprise): on-premises / air-gapped deployment` **Labels**: `enterprise`, `infra`, `P3` **Priority**: P3 | **Size**: XL **Description**: Docker-based on-premises deployment with air-gapped option. Full architecture rethink required — currently all cloud-native (Vercel + Convex + Railway). --- #### Issue #8.10: Android TV app **Title**: `feat(enterprise): Android TV app` **Labels**: `enterprise`, `frontend`, `P3` **Priority**: P3 | **Size**: XL **Description**: Dedicated Android TV app for SOC walls and trading floors. Separate codebase. --- ## Key Risks 1. **Vanilla TS + Clerk**: `@clerk/clerk-js` headless is less documented than React SDK. Prototype early. 2. **Edge + Convex plan lookups**: Vercel Edge can't import Convex. Must cache in Upstash Redis with active invalidation on webhook events. 3. **Sub-60s refresh at scale**: 10x more requests from pro users. SSE/WebSocket needed long-term. 4. **API as separate product**: Multiple Stripe subscriptions per user adds billing complexity. `entitlements` projection table mitigates scattered logic. 5. **Desktop auth + Tauri WKWebView**: Known limitations. PKCE flow with `worldmonitor://` deep link callback. 6. **API key migration outage**: Dual-read rollout (old + new in parallel) with comparison metrics before cutover. ## Observability & Operations - **Audit logging**: all auth events, key lifecycle, billing changes, entitlement decisions → `auditLog` table - **Structured metrics**: entitlement cache hit/miss ratio, webhook processing latency, API key validation latency - **Alerting**: Slack/PagerDuty for webhook failures, entitlement errors, rate limit abuse spikes - **Incident rollback plan**: - Auth cutover: feature flag to disable Clerk and revert to static API key validation - Payment cutover: Stripe test mode for staging; webhook replay via Stripe Dashboard - Migration rollback: `registrations` table preserved 30 days alongside `users` ## Data Retention & Privacy - **API usage data**: retained 90 days, then aggregated to monthly summaries - **Audit logs**: retained 1 year, then archived to cold storage - **AI-generated briefs**: retained 30 days per user, older briefs auto-deleted - **PII handling**: email + name stored in Convex (encrypted at rest). No PII in Redis cache. No PII in outbound delivery payloads. - **Account deletion**: Clerk `user.deleted` webhook → delete user data. **Audit logs and billing records are NOT deleted** — user identifiers anonymized/tombstoned (`deleted-{hash}`). Stripe customer marked deleted; invoice history retained by Stripe.