Appearance
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
| Question | Ruling |
|---|---|
| Whether a safety read may require a connection at all | ADR-0005 |
| Which platform the prototype is built on, and what that costs | ADR-0020 |
| The native-Android argument that was outweighed, not refuted | ADR-0019 (superseded) |
| What the server holds versus what the device holds | ADR-0020, Specification §2 |
| What the device does when its Corpus is gone | ADR-0032 |
| What may cross the network at read time, and what the online tier receives | ADR-0031 |
| Which provider and model that tier calls, and what is exposed by calling it | ADR-0033 |
| Whether the server carries accounts, sessions or a sync of user state | ADR-0029 |
| Where Curation runs, and why the bind is the access control | ADR-0048 |
| Whether a stored copy of an admitted document can be removed through the API | ADR-0058 |
| Where the two application repositories sit relative to this one, and what each checkout resolves against | ADR-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.
| Part | As ADR-0020 names it |
|---|---|
| Frontend | Vue 3 + Vite, Pinia (with persisted state), vue-router, vue-i18n, PrimeVue, Tailwind, Zod |
| Backend | ElysiaJS on Bun, Prisma against Postgres |
| Language and runtime | TypeScript throughout; Bun as package manager and runtime |
| Tests | Vitest 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 Context | Vue 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.
| Part | In use |
|---|---|
| Frontend framework | Vue 3.5, Vite 7, vue-router 5, Pinia 3 |
| Styling | PrimeVue 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 delivery | vite-plugin-pwa 1.3 — precached shell; the Corpus in IndexedDB through hand-written code, not a library |
| Identification | jsqr 1.4 behind the browser's native BarcodeDetector; qrcode-generator 2.0 for the curator's printable labels |
| Curation | pdfjs-dist 5.7 — the page image a curator verifies against is rendered in their own browser and never stored |
| Frontend tests | Vitest 4 with happy-dom, @vue/test-utils, Playwright 1.61 for Seam 4 |
| Backend | Elysia 1.4 on Bun, Prisma 7 with @prisma/adapter-pg against Postgres |
| Backend tests | bun test, no database — the serialiser is a pure function over rows |
| Language | TypeScript 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 path | Ruling |
|---|---|
| Whether the Conversational surface writes safety text or points at it | ADR-0031, on top of ADR-0002 |
| What the provider receives and what it may never receive | ADR-0031 |
| What happens to that path with no signal | ADR-0031, Specification §9 |
| The model, the tier, the spend cap and the residual exposure | ADR-0033 |
| Where the emergency phrase list is evaluated | ADR-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
| Mechanism | Where |
|---|---|
| Seam 4 — no safety read touches the network | Specification, Testing Decisions |
| Seam 5 — Routing resolves to the right place, or to Not Stated | Specification, Testing Decisions |
| Offline as a whole-application property rather than a per-feature one | Specification, Testing Decisions |
| The client/server split written out field by field | Specification §2 |
What is actually built against any of this is Current state & blockers.