Skip to Content
Portal (apps/portal)Architecture

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.jsonc points main here. It re-exports TanStack Start’s @tanstack/react-start/server-entry virtual module wrapped in @microlabs/otel-cf-workers instrument() so every request emits an OpenTelemetry span (with instrumentGlobalFetch: false — the global fetch monkey-patch broke WorkOS session refresh in prod).
  • Router — apps/portal/src/router.tsx. createRouter over the generated routeTree.gen.ts (file-based routes in src/routes/), defaultPreload: "intent", and a themed defaultNotFoundComponent (ResourceNotFound) so unmatched paths and notFound() throws land on the canonical existence-leak-safe 404.
  • Client entry — apps/portal/src/client.tsx. Hydrates with StartClient, 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’s beforeLoad on SSR document requests only. See Auth & Tenancy.
  • Data access — route loaders call createServerFn transports which dynamically import *-client.server.ts adapters that forward the session JWT + active-tenant header to apps/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.

Last updated on