Skip to content

System architecture

The question this answers: what runs where, and what never crosses the network?

The platform was decided twice. The live ruling is ADR-0020 — The prototype is a web application on the work-permit stack. It supersedes ADR-0019, which argued for a native Android prototype and is marked Superseded — do not follow this ruling; ADR-0019 is worth reading anyway, because ADR-0020 carries its argument forward as costs rather than discarding it.

The topology

Every term in that diagram is defined in the glossary — Corpus, Source Span, Routing Floor, Query Intent and Escalation Trigger — and the diagram draws the shape the rulings below produce rather than restating any of them. What the device does when the IndexedDB box is empty is a state of its own, Corpus Unavailable, settled in ADR-0032.

What each ruling settles

QuestionRuling
Whether a safety read may require a connection at allADR-0005
Which platform the prototype is built on, and what that costsADR-0020
The native-Android argument that was outweighed, not refutedADR-0019 (superseded)
What the server holds versus what the device holdsADR-0020, Specification §2
What the device does when its Corpus is goneADR-0032
What may cross the network at read time, and what the online tier receivesADR-0031
Which provider and model that tier calls, and what is exposed by calling itADR-0033
Whether the server carries accounts, sessions or a sync of user stateADR-0029
Where Curation runs, and why the bind is the access controlADR-0048
Whether a stored copy of an admitted document can be removed through the APIADR-0058
Where the two application repositories sit relative to this one, and what each checkout resolves againstADR-0063

The stack, as ADR-0020 names it

Quoted from ADR-0020, which is the document that decides it. The versions in the last row are the ones that ADR records as verified from the sibling work-permit project's source.

PartAs ADR-0020 names it
FrontendVue 3 + Vite, Pinia (with persisted state), vue-router, vue-i18n, PrimeVue, Tailwind, Zod
BackendElysiaJS on Bun, Prisma against Postgres
Language and runtimeTypeScript throughout; Bun as package manager and runtime
TestsVitest and Vue Test Utils
Offline mechanism (D-39)Service worker with a precached app shell; the Corpus held client-side in IndexedDB
Versions recorded in ADR-0020's ContextVue 3.5 + Vite 8, Pinia, vue-router 5, vue-i18n 11, PrimeVue 4.5, Tailwind 4, Zod 4; Elysia 1.4 on Bun 1.3, Prisma 7 with @prisma/adapter-pg

The stack as it is actually installed

The table above is what ADR-0020decides. This one is what the two application repositories contain, read from their package.json manifests on 2026-09-20. The two differ, and the differences are the interesting part.

PartIn use
Frontend frameworkVue 3.5, Vite 7, vue-router 5, Pinia 3
StylingPrimeVue 4.5 unstyled, Tailwind 4 via @tailwindcss/vite, tailwind-merge
Typography@fontsource/ibm-plex-sans-thai 5.3, self-hosted rather than linked, asserted by its own browser case alongside Seam 4
Offline deliveryvite-plugin-pwa 1.3 — precached shell; the Corpus in IndexedDB through hand-written code, not a library
Identificationjsqr 1.4 behind the browser's native BarcodeDetector; qrcode-generator 2.0 for the curator's printable labels
Curationpdfjs-dist 5.7 — the page image a curator verifies against is rendered in their own browser and never stored
Frontend testsVitest 4 with happy-dom, @vue/test-utils, Playwright 1.61 for Seam 4
BackendElysia 1.4 on Bun, Prisma 7 with @prisma/adapter-pg against Postgres
Backend testsbun test, no database — the serialiser is a pure function over rows
LanguageTypeScript throughout; Bun as package manager in both repositories

Two things ADR-0020 names are not installed, and the spec says so rather than implying otherwise — that is what the STACKCLAIMS gate exists for. vue-i18n is absent, so every Thai string is inline. Zod is absent, so payload validation is hand-written type guards. Neither is a silent omission; both are recorded in Specification, Testing Decisions, whose stack-claims block now declares twelve packages and is what the gate reads.

Several installed packages belong to the template, not to this product. Both repositories began as copies of the sibling work-permit project. The frontend still carries axios, js-cookie and humps — the Corpus sync uses plain fetch, and the cookie and auth scaffolding is dead under ADR-0029. The api still carries mailgun.js, nodemailer, minio, ioredis and xlsx, and its file and upload modules are present in the tree but deliberately not mounted — only the corpus module is. A dependency in a manifest is not part of an architecture, and the gate that checks stack claims checks the spec's claims about what is installed, not the reverse.

Why this page has two platform documents instead of one

ADR-0020 did not delete ADR-0019's reasoning; it carried the two costs that reasoning predicted into its own text as D-41 and D-42, and the specification repeats both with the action each one still needs under "Risks carried by the platform decision".

  • D-41, what the platform costs camera Identification. ADR-0020 states it and names it the largest single cost of the stack decision; the three Identification routes themselves are settled in ADR-0030, which supersedes ADR-0004. The spike this still needs is listed as open in Specification, Risks.
  • D-42, what the platform costs offline storage. ADR-0020 records the risk against the assumption ADR-0005 was written under, and what the app does when that risk lands is settled in ADR-0032. The spec marks this one closed.

What the online tier is

Where the network appears in the topology at all is the subject of the rulings below, and they are the documents to read before changing anything on that edge of the diagram.

Question about that pathRuling
Whether the Conversational surface writes safety text or points at itADR-0031, on top of ADR-0002
What the provider receives and what it may never receiveADR-0031
What happens to that path with no signalADR-0031, Specification §9
The model, the tier, the spend cap and the residual exposureADR-0033
Where the emergency phrase list is evaluatedADR-0031

How a user reaches that path in the first place is in Identification & Routing; what the surface may and may not emit once it gets there is The content split.

Where the topology is enforced

MechanismWhere
Seam 4 — no safety read touches the networkSpecification, Testing Decisions
Seam 5 — Routing resolves to the right place, or to Not StatedSpecification, Testing Decisions
Offline as a whole-application property rather than a per-feature oneSpecification, Testing Decisions
The client/server split written out field by fieldSpecification §2

What is actually built against any of this is Current state & blockers.