Portal Architecture
apps/portal is the OpsDNA web app: a TanStack Start React application, server-side rendered on Cloudflare Workers, served at app.opsdna.ai (staging: staging-app.opsdna.ai). It is deliberately edge-stateless: the Worker has no DATABASE_URL (apps/portal/wrangler.jsonc, OPS-83). Every piece of data the portal shows comes over HTTPS from apps/api (or from serialized projections), never from a database connection on the request path. That single decision shapes everything else on this page: the auth gate must be cheap and cookie-only, tenancy must ride a header, and route components must be display-only.
Why it’s built this way
The portal renders financial data for multiple tenants (GP firms, LPs, auditors). The real authorization boundary is apps/api — JWKS-verified bearer JWTs plus Postgres tenant_id FORCE RLS. The portal’s job is UX: don’t render app chrome to anonymous or unprovisioned users, forward the user’s identity and active tenant faithfully, and display projections. Keeping the Worker stateless means a portal bug can degrade the experience but cannot widen data access.
Request flow
The pieces
- Worker entry —
apps/portal/src/otel-worker-entry.ts.wrangler.jsoncpointsmainhere. It re-exports TanStack Start’s@tanstack/react-start/server-entryvirtual module wrapped in@microlabs/otel-cf-workersinstrument()so every request emits an OpenTelemetry span (withinstrumentGlobalFetch: false— the global fetch monkey-patch broke WorkOS session refresh in prod). - Router —
apps/portal/src/router.tsx.createRouterover the generatedrouteTree.gen.ts(file-based routes insrc/routes/),defaultPreload: "intent", and a themeddefaultNotFoundComponent(ResourceNotFound) so unmatched paths andnotFound()throws land on the canonical existence-leak-safe 404. - Client entry —
apps/portal/src/client.tsx. Hydrates withStartClient, and installs stale-asset recovery, deployment-version recovery, and frontend tracing before hydration. - Gate chain — the login gate and active-grant gate run in
__root.tsx’sbeforeLoadon SSR document requests only. See Auth & Tenancy. - Data access — route loaders call
createServerFntransports which dynamically import*-client.server.tsadapters that forward the session JWT + active-tenant header toapps/api. See Data Access. - ~35
portal-*packages factor session crypto, permissions, projections, UI, and observability out of the app. See Package Family.
src/ssr.tsx is dead code in production. It is not the server entry for either build target — verified empirically with a console.log marker that never appeared in bun run build or bun run build:cf output. The real entry is the TanStack virtual server-entry module, resolved directly and wrapped by otel-worker-entry.ts. Editing ssr.tsx does nothing. (See the header comment in otel-worker-entry.ts.)
Deploying
Merging to staging auto-deploys the staging portal; prod is a manual workflow_dispatch from main (see Infra). One sharp edge from wrangler.jsonc: never run wrangler deploy --env staging. The @cloudflare/vite-plugin build flattens the config to one resolved environment, so --env staging falls into legacy-env mode and keeps the prod routes — hijacking app.opsdna.ai. Select the environment at build time instead: CLOUDFLARE_ENV=staging bun run build:cf && wrangler deploy --config dist/server/wrangler.json (what deploy:cf:staging does).
Design source of truth
All visual decisions — fonts, colors, spacing, component posture — live in the repo’s DESIGN.md. Read it before any UI change; review mode flags deviations.