Skip to Content
ArchitectureSystem Overview

System Overview

OpsDNA is one Postgres database with several carefully separated planes around it. Every plane β€” the browser portal, the Lambda API, the bank/document ingestion pipelines, the query/policy runtime β€” ultimately reads and writes the same tenant-isolated RDS Postgres instance, and every plane is designed so that the database (with FORCE row-level security) remains the authoritative boundary, not any code in front of it. The planes are separated because they fail differently β€” an edge Worker, a Lambda, and an SQS consumer have different trust levels, blast radii, and replay semantics β€” and keeping each one narrow is why a bug in one is not a breach of the whole.

The serving plane

The portal (apps/portal) is a TanStack Start React SSR app on a Cloudflare Worker. It is deliberately edge-stateless: it holds no database connection and no durable data β€” its job is the session cookie, the login and active-grant gates, and typed server-fn adapters that forward every data request to apps/api with a Bearer WorkOS JWT plus the untrusted x-opsdna-active-tenant selection header. Verify gates (portal:display-only-check and friends) enforce that portal code never runtime-imports persistence or ledger internals. See Portal and Auth & Tenancy.

The API (apps/api) is a family of single-purpose Lambdas behind API Gateway β€” one esbuild-tree-shaken entry per route group, composed from typed HttpApiGroup contracts, handler layers, and auth middleware (WorkOS JWT, personal API tokens, Plaid JWS). This is the real authorization boundary: resolveTenant narrows the request to exactly one entitled tenant (Wall 1), then Postgres FORCE RLS keyed on app.tenant_id enforces it (Wall 2). Database access goes through RDS Proxy with per-connection IAM tokens and role-scoped pools (opsdna_app, identity_writer, the source-document maintenance role). See API and Tenancy & RLS.

The ingestion planes

Bank data arrives as Plaid webhooks at a deliberately tiny, VPC-less Lambda that only verifies the Plaid JWS signature and forwards the raw payload to SQS. The plaid-sync-worker consumes the queue, drains /transactions/sync, appends immutable transaction versions, and promotes attributed transactions into operating_source_event candidates under deterministic ids (ose:bank:${version.id}) β€” so every replay converges instead of duplicating. From there a human-gated promotion seam turns accepted events into ledger_source_event posting instructions and finally journal entries. See Source Pipeline and Domain Seams.

Documents land in an S3 landing bucket, are malware-scanned out-of-band by GuardDuty, and only post-clean do drain workers run MIME sniffing, classification, and extraction β€” with per-tenant encryption and crypto-shred designed in ADR-0030. See Source Documents.

The deterministic core

Underneath both serving and ingestion sits the money core: the ledger kernel (posting plans, branded-bigint Money, deterministic account ids), the Application/Trace/Row record contract, the policy IR for fund economic terms, and the Acceptance Lab, which rebuilds whole fund scenarios byte-identically to prove determinism end to end. The entity model supplies the spine every one of these keys on.

The runtime plane

The query/policy runtime β€” evidence graph, semantic and computation registries, permission engine, typed query plans, DomainPolicies β€” is where LLMs participate, and it is built so they participate as narrowly as possible: a planner LLM emits untrusted candidates, deterministic code resolves and validates everything else, and every answer or policy run is sealed into an audit record with pinned registry versions and canonical-JSON hashes.

Where things run

PlaneWhereState
PortalCloudflare Worker (app.opsdna.ai, staging staging-app.opsdna.ai)Stateless; session cookie only
API + workersAWS Lambda (per-entry), API Gateway, SQS, EventBridgeStateless; all state in Postgres
DatabaseRDS Postgres 18 behind RDS ProxyThe system of record
DocumentsS3 (landing β†’ clean buckets), GuardDutyBytes never leave the AWS account
Chat agentCloudflare Workers + Durable Objects (apps/chat-agent, Bedrock)Separate inference stack from packages/ai
TracingHoneycomb β€” portal Worker starts the trace, API continues traceparentβ€”

Deploys follow feature branch β†’ staging (auto-deploy) β†’ main (manual prod dispatch), always migrating the database before shipping code β€” see Infra and Branching & Releases.

Next: the design principles that make these planes feel like one system, and the ADR index for the decision history.

Last updated on