%% docs/architecture.mmd %% %% Novel Systems — request flow, as merged. %% %% This diagram describes what is in `main` and deployed, not what the PRD %% plans. A dashed border means the node is a third party outside our control: %% the edge exists in code and is tested up to the provider boundary, and what %% happens past that boundary is theirs. %% %% WHAT THIS DIAGRAM DELIBERATELY DOES NOT SAY. %% %% Until 5 August 2026 the three provider nodes carried their own readiness on %% their faces — "awaiting STRIPE_SECRET_KEY", "awaiting tenant consent" — and %% were drawn dashed amber to match. Every one of those labels was wrong by the %% time anyone read it. Stripe had been transacting in test mode for a day and %% `GET /api/connectors` reported it `platform_account_live`; the other two were %% `operator_credentials_missing`, which is a further-back state than the %% consent screen the label described. %% %% The mistake was not the labels being stale. It was writing a perishable fact %% into an artifact that is rendered by hand and can only be corrected by hand. %% lib/connector-readiness.ts states the rule about itself and it applies here %% with more force, not less: whether a connector can transact today is derived %% from the deployment at request time and published at /integrations. A %% hand-rendered PNG is the worst possible carrier for it. %% %% So these nodes now state only the durable structural fact — which trigger %% fires the call, whether credentials are platform-wide or per-tenant, which %% object gets written — and the reader is sent to /integrations for the state. %% `scripts/check-diagram-current.mjs` fails the build if readiness vocabulary %% comes back into a provider node. %% %% Render: double-click render-diagram.command in the repository root. It drives %% the Chrome already installed on the machine and writes %% docs/architecture.png beside this file. mermaid-cli is not used — %% it needs a Chromium that does not exist for arm64 Linux, which is %% what this repository is built on. The script explains the rest. %% %% Read it as three horizontal bands. The top band is what a browser touches. %% The middle is the two Vercel projects. The bottom is Postgres, and the line %% across it — the RLS boundary — is the one structural claim this platform %% makes that is worth drawing: every query the API issues runs as a %% non-privileged role inside a transaction that has declared which tenant it %% is acting for. flowchart TB %% ── Clients ──────────────────────────────────────────────────────────── subgraph clients["Clients"] direction LR visitor["Anonymous visitor
novelsystems.ca"] operator["Authenticated operator
ADMIN · SALES"] tech["Technician
mobile, TECHNICIAN"] integrator["Integrator's own system
ERP · dispatch board"] end %% ── Edge ─────────────────────────────────────────────────────────────── cloudflare["Cloudflare DNS
novelsystems.ca, www, docs, status"] %% ── Frontend ─────────────────────────────────────────────────────────── subgraph frontend["Vercel · novelsystems.ca — Next.js 14 App Router"] direction TB pages["Server components
marketing, /developers, /security"] sandbox["CPQ sandbox
components/cpq — browser engine"] browserEngine["lib/cpq-engine.ts
bundled to the browser
cut list · 50% floor"] proxy["app/api/cpq/calculate
server-side credential"] forms["app/api/{contact,careers,support,quote}"] end %% ── Backend ──────────────────────────────────────────────────────────── subgraph backend["Vercel · novel-systems-backend — Express + TypeScript"] direction TB subgraph chain["Middleware, in order"] direction LR helmet["helmet"] --> corsmw["cors
allowlist"] --> rawmw["raw 1mb
webhook path only"] --> bodymw["json 256kb"] --> ratemw["rateLimit
300/min"] end health["GET /api/health
select 1 · exempt from the limiter"] login["POST /api/v1/auth/login
argon2 · 10 per 15 min per account"] authmw["requireAuth
JWT, 900s · re-checked against the row"] rolemw["requireRole
ADMIN · SALES · TECHNICIAN"] quotes["/api/v1/quotes
calculate · list · approve"] workorders["/api/v1/work-orders
list · create · status"] serverEngine["services/cpq-engine.ts
never bundled to a browser
cut list · 50% floor · rate cards"] dispatcher["services/webhook-dispatcher.ts
HMAC · 5 attempts · ~1 min"] tenantctx["db/tenant.ts — withTenant()
set_config('app.current_organization_id', …, local)"] subgraph integlayer["src/integrations — provider layer"] direction TB integroutes["/api/v1/integrations
list · connect · callback · disconnect · syncs"] stripehook["POST /integrations/stripe/webhook
unauthenticated · signature is the auth"] oauthstate["oauth-state.ts
HMAC state · 600s · signature checked first"] envelope["crypto.ts — AES-256-GCM
v1.iv.tag.ciphertext
refresh tokens never land in plaintext"] fanout["fan-out.ts
runs on quote.approved, after commit"] connstore["connection-store.ts
one row per tenant × provider"] syncstore["sync-store.ts
idempotency + attempt counter"] end end %% ── Data ─────────────────────────────────────────────────────────────── subgraph data["Supabase Postgres 15 · ca-central-1"] direction TB rls{{"ROW LEVEL SECURITY
role novel_app · FORCE RLS
every table, per-tenant policy"}} coretables[("organizations · users
inventory_skus · labor_tiers
quotes · work_orders
webhooks · webhook_deliveries
integration_connections · integration_syncs")] lookupfn["integration_sync_lookup()
SECURITY DEFINER
the only way in without a tenant"] sitetables[("leads · job_applications
support_tickets · quotes (marketing)
audit_events")] end %% ── Outbound ─────────────────────────────────────────────────────────── resend["Resend
notification mail"] subscriber["Subscriber endpoint
customer-registered"] %% Third parties. Dashed because the far side of the edge is not ours, not %% because of anything about how ready it is — see the header. Each label is %% the durable fact: where the credential lives, and what object the adapter %% writes when quote.approved fires. stripe["Stripe
platform credential · one account
writes an invoice"] quickbooks["QuickBooks Online
per-tenant OAuth
writes an invoice"] salesforce["Salesforce
per-tenant OAuth
writes an opportunity"] %% The one place readiness is allowed to appear, and it appears as an address %% rather than as a value. readiness["Which of these can transact today
is derived per deployment, not drawn here.
GET /api/connectors · published at /integrations"] %% ── Flows ────────────────────────────────────────────────────────────── visitor --> cloudflare operator --> cloudflare tech --> cloudflare cloudflare --> pages cloudflare --> proxy integrator -->|"Bearer JWT, direct"| chain pages --> sandbox sandbox -->|"instant, no network"| browserEngine sandbox -->|"priced quote"| proxy proxy -->|"service credential
8s timeout"| chain forms --> resend forms -->|"service_role key
bypasses RLS by design"| sitetables chain --> health chain --> login chain --> stripehook chain --> authmw authmw --> rolemw rolemw --> quotes rolemw --> workorders rolemw -->|"ADMIN only"| integroutes quotes --> serverEngine quotes --> tenantctx workorders --> tenantctx serverEngine --> tenantctx health -.->|"select 1"| data tenantctx ==>|"one transaction
one tenant"| rls rls --> coretables quotes -->|"quote.approved
after commit, not awaited"| dispatcher workorders -->|"work_order.*"| dispatcher dispatcher -->|"POST · x-novel-signature"| subscriber dispatcher --> coretables %% ── Integrations ─────────────────────────────────────────────────────── quotes ==>|"quote.approved
same trigger as the dispatcher"| fanout fanout --> syncstore fanout --> connstore integroutes --> oauthstate integroutes --> connstore integroutes --> syncstore connstore --> envelope connstore --> tenantctx syncstore --> tenantctx %% An inbound Stripe delivery names a Stripe invoice, not a tenant. The %% SECURITY DEFINER function is the one read allowed to answer "whose is %% this?" — after which everything reverts to withTenant. stripehook -->|"raw bytes
HMAC · 300s tolerance"| syncstore syncstore -.->|"resolveByExternalId"| lookupfn lookupfn --> coretables fanout -.->|"invoice"| stripe fanout -.->|"invoice"| quickbooks fanout -.->|"opportunity"| salesforce stripe -.- readiness readiness -.- salesforce oauthstate -.->|"authorize · consent · token exchange"| quickbooks oauthstate -.->|"authorize · consent · token exchange"| salesforce stripe -.->|"invoice.paid
invoice.payment_failed"| stripehook %% ── The claim worth drawing ──────────────────────────────────────────── note["The two engines cannot import from each other —
one ships to a browser, one must not.
They are held together by fixtures/cut-list-parity.json
and by check:cpq-parity in CI. See D-002, D-010."] browserEngine -.- note note -.- serverEngine %% ── Styling ──────────────────────────────────────────────────────────── %% Renamed from `unbuilt` on 5 August 2026. The old name asserted a fact about %% our code — that these paths did not exist — which was never true of any of %% them and had stopped being defensible about Stripe. Third-party is a %% property of the node that cannot go stale. classDef thirdparty stroke-dasharray:6 4,stroke:#0f766e,color:#0f766e,fill:#f0fdfa classDef boundary fill:#fee2e2,stroke:#b91c1c,stroke-width:2px,color:#7f1d1d classDef engine fill:#eef2ff,stroke:#4338ca,color:#312e81 classDef store fill:#f0fdf4,stroke:#15803d,color:#14532d classDef annotation fill:#f8fafc,stroke:#94a3b8,stroke-dasharray:3 3,color:#475569 class stripe,quickbooks,salesforce,resend,subscriber thirdparty class readiness annotation class rls,tenantctx,envelope,lookupfn boundary class browserEngine,serverEngine engine class coretables,sitetables store class note annotation