ARCHITECTURE.md
Capability counts (map layers, services, protos, locales, CI workflows, freshness sources) are derived from code and CI-verified by
npm run docs:check(scripts/docs-stats.mjs, source of truthdocs/generated/stats.json). Do not hand-edit those numbers — change the code, runnpm run docs:stats.Ownership rule: When deployment topology, API surface, desktop runtime, or bootstrap keys change, this document must be updated in the same PR.
Design philosophy: For the "why" behind architectural decisions, intelligence tradecraft, and algorithmic choices, see Design Philosophy.
World Monitor is a real-time global intelligence dashboard built as a TypeScript single-page application. It aggregates data from dozens of external sources covering geopolitics, military activity, financial markets, cyber threats, climate events, maritime tracking, and aviation into a unified operational picture rendered through an interactive map and a grid of specialized panels.
┌─────────────────────────────────────────────────────────────────┐
│ Browser / Desktop │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ DeckGLMap│ │ GlobeMap │ │ Panels │ │ Workers │ │
│ │(deck.gl) │ │(globe.gl)│ │(Panel base)│ │(ML, analysis)│ │
│ └────┬─────┘ └────┬─────┘ └─────┬──────┘ └──────────────┘ │
│ └──────────────┴──────────────┘ │
│ │ fetch /api/* │
└─────────────────────────┼───────────────────────────────────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌──────▼──────┐ ┌─────▼─────┐ ┌─────▼──────┐
│ Vercel │ │ Railway │ │ Tauri │
│ Edge Funcs │ │ AIS Relay │ │ Sidecar │
│ + Middleware│ │ + Seeds │ │ (Node.js) │
└──────┬──────┘ └─────┬─────┘ └─────┬──────┘
│ │ │
└──────────────┼──────────────┘
│
┌──────▼──────┐
│ Upstash │
│ Redis │
└──────┬──────┘
│
┌───────────┼───────────┐
│ │ │
┌─────▼───┐ ┌─────▼───┐ ┌────▼────┐
│ Finnhub │ │ Yahoo │ │ ACLED │
│ OpenSky │ │ GDELT │ │ UCDP │
│ CoinGeck│ │ FRED │ │ FIRMS │
│ ... │ │ ... │ │ ... │
└─────────┘ └─────────┘ └─────────┘
536+ observed upstream hosts
Source files: package.json, vercel.json
| Service | Platform | Role |
|---|---|---|
| SPA + Edge Functions | Vercel | Static files, API endpoints, middleware (bot filtering, social OG) |
| CORS Preflight Worker | Cloudflare | Edge CORS for api.worldmonitor.app — short-circuits OPTIONS, stamps CORS headers on responses |
| AIS Relay | Railway | WebSocket proxy (AIS stream), seed loops (market, aviation, GPSJAM, risk scores, UCDP, positive events), RSS proxy, OREF polling |
| Consumer Prices | Railway | Containerized price scrapers (Playwright, per-country baskets) + Redis publisher for the consumer-prices dataset |
| Redis | Upstash | Cache layer with stampede protection, seed-meta freshness tracking, rate limiting |
| Convex | Convex Cloud | Billing/entitlements (Dodo), user state and API keys, broadcast/email, contact + waitlist forms, historical intelligence memory (vector search) |
| Documentation | Mintlify | Public docs, proxied through Vercel at /docs |
| Desktop App | Tauri 2.x | macOS (ARM64, x64), Windows (x64), Linux (x64, ARM64) with bundled Node.js sidecar |
| Container Image | GHCR | Multi-arch Docker image (nginx serving built SPA, proxies API to upstream) |
Source files: vercel.json, docker/Dockerfile, scripts/ais-relay.cjs, consumer-prices-core/Dockerfile, workers/api-cors-preflight/wrangler.toml, convex/schema.ts, src-tauri/tauri.conf.json
Cloudflare zone config (dashboard-managed, NOT in this repo): the apex worldmonitor.app → www 301 is a Cloudflare Dynamic Redirect rule ("apex to www (exclude agent-discoverable paths)") whose exemption list is load-bearing: /.well-known/*, /robots.txt, /security.txt, /mcp, /mcp/*, and /oauth/* are served on the apex, never redirected. Dropping the /mcp* exemptions breaks every apex-URL MCP client; dropping /oauth/* re-breaks OAuth dynamic client registration — a redirected POST becomes a GET and dies with 405 (issue #4938). When editing the rule, mind expression precedence: and binds tighter than or, so a new exemption must be added as its own or term inside the not (…) group (appending and not … after the last term is a silent no-op). mcp-live-smoke.yml probes the MCP/OAuth members of this list (/mcp, /.well-known/oauth-authorization-server, and the OAuth endpoints it declares) every 6 hours and fails on the redirect fingerprint; the robots.txt / security.txt exemptions are crawler-facing and have no automated probe.
src/main.ts initializes Sentry error tracking, Vercel analytics, dynamic meta tags, runtime fetch patches (desktop sidecar redirection), theme application, and creates the App instance.
App.init() runs in 8 phases:
/api/bootstrap (fast 3s + slow 5s timeouts)loadAllData() + viewport-conditional primeVisiblePanelData()startSmartPollLoop()All panels extend the Panel base class (109 classes across src/components). Panels render via setContent(html) (debounced 150ms) and use event delegation on a stable this.content element. Panels support resizable row/col spans persisted to localStorage.
htmlElementsData array with _kind discriminator. Earth texture, atmosphere shader, auto-rotate after idle.Layer definitions live in src/config/map-layer-definitions.ts, each specifying renderer support (flat/globe), premium status, variant filtering, and i18n keys.
No external state library. AppContext is a central mutable object holding: map references, panel instances, panel/layer settings, all cached data (news, markets, predictions, clusters, intelligence caches), in-flight request tracking, and UI component references. URL state syncs bidirectionally via src/utils/urlState.ts (debounced 250ms).
@xenova/transformers (MiniLM-L6 embeddings, sentiment, summarization, NER), in-worker vector store for headline memoryDetected by hostname (tech.worldmonitor.app → tech, finance.worldmonitor.app → finance, etc.) or localStorage on desktop. Controls: default panels, map layers, refresh intervals, theme, UI text. Variant change resets all settings to defaults.
Source files: src/main.ts, src/App.ts, src/app/, src/components/Panel.ts, src/components/DeckGLMap.ts, src/components/GlobeMap.ts, src/config/variant.ts, src/workers/
The api/ directory holds two kinds of endpoints, both deployed as Vercel Edge Functions:
server/worldmonitor/**. The per-domain thin entry points (api/<domain>/v<N>/[rpc].ts) are produced via createDomainGateway (server/gateway.ts) and esbuild-bundled, so the deployed artifact is self-contained even though the source composes server-side modules.api/create-checkout.ts, api/customer-portal.ts, api/mcp.ts, api/user-prefs.ts).Edge functions are bundled per file: each deployed function may not pull in unrelated modules at runtime, a constraint enforced by tests/edge-functions.test.mjs and the pre-push esbuild bundle check. Hand-written endpoints that genuinely cannot be proto-defined are listed in api/api-route-exceptions.json and enforced by npm run lint:api-contract.
| File | Purpose |
|---|---|
_cors.js | Origin allowlist (worldmonitor.app, Vercel previews, tauri://localhost, localhost) |
_rate-limit.js | Upstash sliding window rate limiting, IP extraction |
_api-key.js | Origin-aware API key validation (desktop requires key, trusted browser exempt) |
_relay.js | Factory for proxying requests to Railway relay service |
server/gateway.ts provides createDomainGateway(routes) for per-domain Edge Function bundles. Pipeline:
{param} scan)| Tier | s-maxage | Use case |
|---|---|---|
| fast | 300s | Live event streams, flight status |
| medium | 600s | Market quotes, stock analysis |
| slow | 1800s | ACLED events, cyber threats |
| static | 7200s | Humanitarian summaries, ETF flows |
| daily | 86400s | Critical minerals, static reference data |
| no-store | 0 | Vessel snapshots, aircraft tracking |
server/worldmonitor/<domain>/v1/handler.ts exports handler objects with per-RPC functions. Each RPC function uses cachedFetchJson() from server/_shared/redis.ts for cache-miss coalescing: concurrent requests for the same key share a single upstream fetch and Redis write.
Source files: api/, server/gateway.ts, server/router.ts, server/_shared/redis.ts, server/worldmonitor/
The project uses the sebuf framework built on Protocol Buffers:
proto/ definitions
↓ buf generate
src/generated/client/ (TypeScript RPC client stubs)
src/generated/server/ (TypeScript server message types)
docs/api/ (OpenAPI v3 specs)
Service definitions use (sebuf.http.config) annotations to map RPCs to HTTP verbs and paths. GET fields require (sebuf.http.query) annotation. repeated string fields need parseStringArray() in the handler. int64 maps to string in TypeScript.
CI enforces generated code freshness via .github/workflows/proto-check.yml: runs make generate and fails if output differs from committed files.
Source files: proto/, Makefile, src/generated/, .github/workflows/proto-check.yml
/api/bootstrap reads cached keys from Redis in a single batch call. The SPA fetches two tiers concurrently (fast + slow) with separate abort controllers and timeouts. Large or opt-in datasets use a public, CDN-shielded single-key request and are consumed through ensureHydrated(key) only when their panel renders. Tier-hydrated data is consumed by panels via getHydratedData(key).
scripts/seed-*.mjs fetch upstream data, transform it, and write to Redis via atomicPublish() from scripts/_seed-utils.mjs. Atomic publish acquires a Redis lock (SET NX), validates data, writes the cache key, writes seed-meta:<key> with { fetchedAt, recordCount }, and releases the lock.
The Railway relay service (scripts/ais-relay.cjs) runs continuous seed loops:
These are the primary seeders. Standalone seed-*.mjs scripts on Railway cron are secondary/backup.
The market backup bundle also persists 14 days of timestamped hourly Yahoo closes for the news-to-market correlation panel. This series is an on-demand bootstrap key, so it does not increase the default hydration payload.
startSmartPollLoop() supports: exponential backoff (max 4x), viewport-conditional refresh (only if panel is near viewport), tab-pause (suspend when hidden), and staggered flush on tab visibility (150ms delays).
api/health.js checks every bootstrap and standalone key. For each key it reads seed-meta:<key> and compares fetchedAt against maxStaleMin. Cascade groups handle fallback chains (e.g., theater-posture: live, stale, backup). Returns per-key status: OK, STALE, WARN, EMPTY.
Source files: api/bootstrap.js, api/health.js, scripts/_seed-utils.mjs, scripts/seed-*.mjs, scripts/ais-relay.cjs, src/services/bootstrap.ts, src/app/refresh-scheduler.ts
Tauri 2.x (Rust) manages the app lifecycle, system tray, and IPC commands:
src-tauri/sidecar/local-api-server.mjs runs on a dynamic port. It dynamically loads Edge Function handler modules from api/, injects secrets from the keyring via environment variables, and monkey-patches globalThis.fetch to force IPv4 (Node.js tries IPv6 first, but many government APIs have broken IPv6).
installRuntimeFetchPatch() in src/services/runtime.ts replaces window.fetch on the desktop renderer. All /api/* requests route to the sidecar with Authorization: Bearer <token> (5-min TTL from Tauri IPC). If the sidecar fails, requests fall back to the cloud API.
Source files: src-tauri/src/main.rs, src-tauri/sidecar/local-api-server.mjs, src/services/runtime.ts, src/services/tauri-bridge.ts
Browser ↔ Vercel Edge ↔ Upstream APIs
Desktop ↔ Sidecar ↔ Cloud API / Upstream APIs
Three CSP sources that must stay in sync:
index.html <meta> tag (development, Tauri fallback)vercel.json HTTP header (production, overrides meta)src-tauri/tauri.conf.json (desktop)API keys are required for non-browser origins. Trusted browser origins (production domains, Vercel preview deployments, localhost) are exempt. Premium RPC paths always require a key.
middleware.ts filters automated traffic: blocks known crawler user-agents on API and asset paths, allows social preview bots (Twitter, Facebook, LinkedIn, Telegram, Discord) on story and OG endpoints.
Per-IP sliding window via Upstash with per-endpoint overrides for high-traffic paths.
Secrets are stored in the platform keyring (never plaintext), injected into the sidecar via Tauri IPC, and scoped to an allowlist of environment variable keys.
Source files: middleware.ts, vercel.json, index.html, src-tauri/tauri.conf.json, api/_api-key.js, server/_shared/rate-limit.ts
Bootstrap seed (Railway writes to Redis on schedule)
↓ miss
In-memory cache (per Vercel instance, short TTL)
↓ miss
Redis (Upstash, cross-instance, cachedFetchJson coalesces concurrent misses)
↓ miss
Upstream API fetch (result cached back to Redis + seed-meta written)
Every RPC handler with shared cache MUST include request-varying parameters in the cache key. Failure to do so causes cross-request data leakage.
server/gateway.ts computes an FNV-1a hash of each response body and returns it as an ETag. Clients send If-None-Match and receive 304 Not Modified when content is unchanged.
CDN-Cache-Control headers give Cloudflare edge (when enabled) longer TTLs than Cache-Control, since CF can revalidate via ETag without full payload transfer.
Every cache write also writes seed-meta:<key> with { fetchedAt, recordCount }. The health endpoint reads these to determine data freshness and raise staleness alerts.
Source files: server/_shared/redis.ts, server/gateway.ts, api/health.js
node:test runner. Test files in tests/*.test.{mjs,mts} cover: server handlers, cache keying, circuit breakers, edge function constraints, data validation, market quote dedup, health checks, panel config guardrails, and variant layer filtering.
api/*.test.mjs and src-tauri/sidecar/*.test.mjs test CORS handling, YouTube embed proxying, and local API server behavior.
Playwright specs in e2e/*.spec.ts test theme toggling, circuit breaker persistence, keyword spike flows, mobile map interactions, runtime fetch patching, and visual regression via golden screenshot comparison per variant.
tests/edge-functions.test.mjs validates that all non-helper api/*.js files are self-contained: no node: built-in imports, no cross-directory ../server/ or ../src/ imports. The pre-push hook also runs an esbuild bundle check on each endpoint.
Runs before every git push:
tsc --noEmit for src and API)Source files: tests/, e2e/, playwright.config.ts, .husky/pre-push
| Workflow | Trigger | Checks |
|---|---|---|
typecheck.yml | PR, push to main | tsc --noEmit for src and API tsconfigs |
lint-code.yml | PR, push to main | Biome lint + sebuf API-contract enforcement |
lint.yml | PR (markdown changes) | markdownlint-cli2 |
test.yml | PR, push to main | Unit/integration suite, docs-stats guardrail, plus conditional digest-image and resilience-validation smoke gates |
proto-check.yml | PR (proto changes) | Generated code matches committed output |
pro-bundle-freshness.yml | PR (pro bundle changes) | Committed pro data bundle artifacts are fresh |
feed-validation.yml | PR (feed changes), daily cron | RSS feed reachability and validation |
mcp-live-smoke.yml | 6-hourly cron, push to main (smoke paths), manual | Anonymous strict-client walk of the production MCP surface on apex + www (capability walk, auth wall, OAuth endpoint routing — #4937/#4938 regression net) |
live-api-cache-auth.yml | 6-hourly cron, push to main (sweep paths), manual | Production cache/auth posture sweep: fake auth stays no-store and is never a cached 200, anonymous public surfaces stay cacheable, MCP/OAuth surfaces stay protocol-valid (#4497 regression net; suite was inert until #5379 wired the gate on, and the step fails if it executes 0 assertions) |
china-decision-parity-live.yml | 6-hourly cron, push to main (audit paths), manual (optional staging URL) | Live half of the China decision-signal parity audit: probes the deployed composition RPC and the public chinaDecisionSignals bootstrap projection for the six-domain contract and a canonical snapshot under one hour old (#5643 — the probe existed but nothing invoked it, and --require-live keeps a lost --url from passing vacuously) |
security-audit.yml | PR, push to main, daily cron, manual | Production dependency audits for every tracked package-lock.json workspace, failing on unbaselined high/critical advisories |
seed-freshness-monitor.yml | 15-minute cron, manual | Enforces production ingestion acceptance after a green scheduled main gate; fails on every actionable compact-health problem except explicitly on-demand sources without grading production before Railway deploys or runs |
railway-deploy-trigger.yml | 10-minute offset cron, manual | Reconciles the Railway fleet forward under a bounded Durable Object lease: deploys each service whose dependency closure changed since the commit it is running, revalidates exact green main before every serial provider call, and counts success only after read-only terminal convergence plus strict zero drift; runner-less runs own no production lock |
analytics-collector-monitor.yml | 15-minute cron, manual | Probes the self-hosted Umami collector directly (heartbeat, tracker script, ingest route) and fails when events are being dropped — Railway reported a green deployment through the 4-day #5565 blackout, so deployment status is not trusted here |
umami-storage-monitor.yml | 15-minute cron, manual | Reads the Umami Postgres Railway volume and the umami-retention deployment history without mutation, caches a bounded history, and fails on capacity or projected days-to-full thresholds, or when the retention runner's newest deployment that ran is CRASHED |
postmerge-deploy-monitor.yml | 10-minute cron, manual | Alarms on a failed post-merge production deploy (#6376): reads the newest completed run on main of convex-deploy.yml, deploy-railway-reconcile-control.yml and deploy-worker.yml and fails when the deploy job did not run/succeed — covers the un-gated deployers outside deploy-gate.yml's PR smoke list |
perf-style-layout-budget.yml | Twice-daily cron, manual (URL + budget inputs) | The #4536 forced-reflow gate the desktop main-thread baseline named but nothing enforced: captures /dashboard with the Playwright harness and fails when the styleLayout share of attributed main-thread self-time exceeds budget. Gates the share, not absolute ms, and runs scheduled rather than per-PR because lab absolutes are host-contention contaminated (KTD1) while the decomposition is stable. A report that measured nothing returns unmeasured, never a pass |
contributor-trust.yml | PR | Gates untrusted first-time-contributor runs |
deploy-gate.yml | After Test/Typecheck/Security Audit complete | Aggregates required smoke-gate statuses onto the head SHA for branch protection |
indexnow-submit.yml | Successful Production deployment, manual | Submits deployment-relevant canonical URLs to IndexNow only after their host-specific ownership keys are directly reachable |
convex-deploy.yml | Push to main, manual | Deploys Convex backend functions |
deploy-worker.yml | Push to main (worker paths), manual | Deploys the api-cors-preflight Cloudflare Worker |
deploy-railway-reconcile-control.yml | Push to main (control-plane paths), manual | Tests and deploys the isolated SQLite-backed Durable Object used for Railway reconciliation leases, attempts, dispatch holds, and the global mutation-uncertain barrier; deployment does not itself activate the trigger cutover |
railway-deploy-trigger-watchdog.yml | 15-minute offset cron, manual | Independently classifies reconcile liveness with bounded GitHub history; observe-only until both cutover and recovery flags are enabled, and then may dispatch one fenced replacement without cancelling, rerunning, or approving any existing production run |
railway-reconcile-manual-recovery.yml | Protected manual dispatch only | Evidence-bound break-glass resolution for ambiguous dispatch holds or post-mutation barriers; records immutable supersession and delegates any retry to the ordinary lease-aware workflow rather than carrying a Railway deploy token |
desktop-release-train.yml | Push to main (release inputs), daily cron, manual | Compares the checked-in desktop version with the latest published release, creates a compatible release tag, and dispatches the atomic multi-platform desktop build |
build-desktop.yml | Release tag, push, manual | Multi-platform Tauri build, code signing (macOS), AppImage library stripping (Linux), smoke test |
docker-publish.yml | Release, manual | Multi-arch image (amd64, arm64) pushed to GHCR |
publish-cli.yml | cli-v* tag, manual | Tests and publishes the worldmonitor npm CLI (cli/) via OIDC trusted publishing (no token) with provenance |
publish-python.yml | py-v* tag, manual | Tests and publishes the worldmonitor-sdk PyPI package (sdk/python/) via OIDC trusted publishing (no token) with attestations |
publish-ruby.yml | gem-v* tag, manual | Tests and publishes the worldmonitor gem (sdk/ruby/) via RubyGems OIDC trusted publishing (no token) |
publish-go.yml | sdk/go/v* tag, manual | Vets/tests the Go SDK module (sdk/go/) at the tag and warms proxy.golang.org so the version is go-gettable and indexed on pkg.go.dev |
test-linux-app.yml | Twice-weekly schedule (Mon/Thu 05:23 UTC), manual | Desktop Canary (Linux): installed-app build + launch, hard-fails on crashed app, unreachable sidecar, or blank render (#5902) |
The Railway umami runtime is built from Dockerfile.umami, which pins the
upstream v3.2.0 release and applies the reviewed session-data upsert fix.
The separate umami-retention cron uses Dockerfile.umami-retention and the
bounded SQL contract. The old collector is drained to zero before the patched
image runs its schema migration as a monitored one-off; schema verification,
patched-runtime write acceptance, and retention are independent operational
gates.
Source files: .github/workflows/, .husky/pre-push. The workflow list is CI-checked against .github/workflows/*.yml by npm run docs:check — a new workflow file must be added to this table.
.
├── api/ Vercel Edge Functions (self-contained JS)
│ ├── _*.js Shared helpers (CORS, rate-limit, API key, relay, Sentry, session)
│ └── <domain>/ Domain endpoints (aviation/, climate/, conflict/, ...)
├── blog-site/ Static blog (built into public/blog/)
├── cli/ Official `worldmonitor` npm CLI (zero-dep ESM, MCP-first; published via cli-v* tag)
├── consumer-prices-core/ Consumer-price collection service (Playwright scrapers, per-country baskets; Railway/Docker)
├── convex/ Convex backend (billing/entitlements, user state, broadcast, forms, intel history)
├── data/ Static data (telegram channels, OREF threat translations, gamma irradiators)
├── deploy/ Deployment configs (nginx)
├── docker/ Dockerfile + nginx config for Railway
├── docs/ Mintlify documentation site
├── e2e/ Playwright E2E specs
├── pro-test/ Standalone Pro QA app (separate package)
├── proto/ Protobuf service definitions (sebuf framework)
├── public/ Static assets served as-is (favicons, textures, .well-known agent-skills/MCP, llms.txt)
├── scripts/ Seed scripts, build helpers, relay service
├── server/ Server-side code (bundled into Edge Functions)
│ ├── _shared/ Redis, rate-limit, LLM, caching utilities
│ ├── gateway.ts Domain gateway factory
│ ├── router.ts Route matching
│ └── worldmonitor/ Domain handlers (mirrors proto structure)
├── shared/ Cross-platform JSON configs (markets, RSS domains)
├── src/ Browser SPA (TypeScript)
│ ├── app/ App orchestration managers
│ ├── bootstrap/ Startup/recovery (chunk reload, deferred Sentry, SW update)
│ ├── components/ Panel subclasses + map components
│ ├── config/ Variant, panel, layer, market configurations
│ ├── data/ Static JSON datasets (conservation, renewable, happiness)
│ ├── e2e/ Map test harnesses (consumed by Playwright specs)
│ ├── embed/ Embeddable widget loader
│ ├── generated/ Proto-generated client/server stubs (DO NOT EDIT)
│ ├── locales/ i18n translation files
│ ├── services/ Business logic organized by domain
│ ├── shared/ Cross-cutting helpers (premium paths, registries, staleness)
│ ├── shims/ Runtime shims (child-process for sidecar)
│ ├── styles/ Global CSS (layers, themes, panel styles)
│ ├── types/ TypeScript type definitions
│ ├── utils/ Shared utilities (circuit-breaker, theme, URL state)
│ └── workers/ Web Workers (analysis, ML, vector DB)
├── src-tauri/ Tauri desktop shell (Rust)
│ └── sidecar/ Node.js sidecar API server
├── tests/ Unit/integration tests (node:test)
└── workers/ Cloudflare Workers (edge CORS preflight for api.worldmonitor.app)