%% 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