Engineering
Twenty-six procedures, one app
Engineering a civic study tool when the content belongs to twenty-six administrations
0. Where this comes from
I am not a Swiss citizen. I moved to Morges in 2014, and in 2026 I started the ordinary naturalisation procedure, which in the canton of Vaud ends with a written knowledge test organised by the commune where you live. I built this app because I needed it, and I kept building it because the parts I found hardest were not the parts I expected. In August 2026 I sat that exam myself and passed it with full marks — 48 out of 48. In the room that morning, several other candidates were using the app.
This is an engineering write-up, not a product pitch. It is about what happens when the thing you are shipping is not really software: it is a claim about facts that belong to somebody else, and that change without telling you.
1. The problem, in one line
Switzerland has no national naturalisation test. Federal law prescribes none, and each of the twenty-six cantons owns its own format. Some publish a complete question catalogue. Some publish study material and no questions. Some assess orally and publish nothing at all. And in one canton the test is not cantonal but municipal: the questions a candidate receives depend on which of three hundred communes they live in.
So the naive model, one question bank and one exam and one app, is wrong in the first five minutes. The real shape is twenty-six different procedures, and inside one of them, three hundred more.
Today the app carries 13,497 exam questions across all twenty-six cantons, of which 9,565 are municipal: questions that exist only for one commune, and are useless to everyone else. Selecting a commune is therefore not a preference. It is the query.
2. Why the hard part is not the app
An app that asks multiple-choice questions is a weekend. The work is everywhere else: obtaining material that twenty-six administrations publish in different formats, in three languages, at their own pace; deciding what each item actually is; and keeping all of it honest for months while nobody tells you anything has moved.
Fourteen cantons now have their own catalogue in the app. The remaining twelve are served the federal layer borrowed at runtime from a canton that does publish one, and the app says so on the question itself. That distinction, this is your canton's material versus this is federal material we are showing you because your canton publishes none, is not a footnote. It is the difference between a study tool and a misleading one.
3. The content chain, and what a question carries
Every question in the app carries its own provenance: where it came from, how it was obtained, on what date, and against which fingerprint of the source. The split is honest about how uneven the landscape is:
| Method | Questions |
|---|---|
| Live cantonal API | 9,693 |
| Text extraction from published PDFs | 1,525 |
| Machine translation, flagged as unofficial | 522 |
| Hand-authored from official published material | 890 |
| Manually curated | 28 |
| Verified manual transcription | 290 |
| Official e-learning material | 126 |
| Other declared forms | 423 |
No question in the app lacks a provenance block. That was a design constraint from early on, and it is the reason the rest of this document is possible.
Where no catalogue exists and the deck has to be written, hand-authoring is not free composition. Each authored question carries an anchor: a verbatim string from an official published document. At build time the generator searches the source corpus for that string, and on three cantons it fails hard if the anchor is not found. No output, no publication. A question that cannot point at a sentence in an official document does not ship.
The same discipline applies to certainty. Exam parameters, how many questions and what pass mark and how long, are not asserted. Each carries a confidence level: official, estimated, reported by the provider, not published, not applicable. Across the cantonal models, forty-nine of these parameters are marked not published, because the canton genuinely does not publish them. Showing that honestly turned out to be more persuasive to institutional readers than a clean-looking number would have been.
4. Nightly surveillance, and the check that nobody writes
Sources move. A canton republishes a PDF, renumbers a series, corrects an answer. Nobody sends a notification.
Thirteen cantons therefore have their published source checked every night by a scheduled job, on staggered hours. Each run fetches the source, fingerprints it, compares it against the stored baseline, and resolves into one of five outcomes: unchanged, changed, source unreachable, refresh failed, or database write failed. Transient network trouble is deliberately distinguished from a real failure, because treating a flaky connection as a broken source produces alarms nobody reads.
Every run appends one signed line to an append-only journal on a separate branch: the timestamp, the source fingerprint, the HTTP status, the fetch duration, and the commit of the code that ran. Reproducibility is not a claim; it is a record.
But the check I would point a reviewer at is a different one, and it exists because of a failure. The drift detector compares upstream sources against a stored baseline. It never asks the one question that matters after publication: does what we are serving still match what we committed? It did not, and one canton served sixty-two questions whose parser fix had been committed six weeks earlier and never published. Silently. Nothing in the pipeline was designed to notice.
So a second comparison now runs on every cycle, including, especially, the cycles where nothing changed. It recomputes the content fingerprint from the question text, the answers and the correct answer rather than trusting the stored stamp, so a stale stamp shows up as a divergence instead of hiding one. A canton nobody is touching is exactly where a divergence sits unnoticed.
5. Updating without the stores
An exam is on a date. If a candidate's canton corrects an answer three weeks before their test, an app that can only ship through a store review is not a study tool, it is a liability.
So content does not travel with the binary. Approved corrections reach devices through a differential channel: on launch and on return to the foreground, the client asks for everything above its own cursor and merges it over the bundled catalogue. No store submission, no review queue, no waiting for users to update.
The number, measured on production this week: of 12,744 published questions, 541, or 4.2 per cent, have been revised at least once since first publication and delivered this way. The distribution is uneven and tells its own story. The largest counts: 148 in Valais, 140 in Vaud, 112 in Lucerne, 43 in Basel-Stadt, 36 in Neuchâtel, 27 in Zurich, 24 in St. Gallen. One canton's questions have reached their fifth version.
Two safeguards make this defensible rather than reckless.
Nothing publishes itself. When drift is detected the pipeline regenerates, writes to the database, and marks the canton awaiting review. In that state the client refuses to sync: users keep seeing the previously approved content. Promotion to live is manual, always, and the pipeline is structurally forbidden from doing it. Right now all thirteen published cantons sit in live.
Nothing is overwritten silently. Every modification archives the complete previous version of the question, text and answers and correct answer and format, before the new one lands.
The fastest cycle so far: an integration officer in a German-speaking region reported that long answers were being truncated. The cause was ours, and worse than cosmetic. The truncation was applied to the correct answer but not to the distractors, so the right answer was systematically the short one ending in an ellipsis. A tell, in an exam trainer. The repair touched 147 of that canton's 280 rows across both languages, and production recorded the content change two minutes after the fix was committed, the same evening as the report.
6. Transparency as a function, not a page
Most apps treat transparency as a document. Here it is a runtime property, and it shows up in three places.
On the question. Under every question, in practice and in mock exams, the candidate sees where it came from, whether its format was adapted from the original, whether it was borrowed from another canton's official pool, whether it was translated, and a verification badge carrying the date the source was last checked.
Before the exam. The mock exam declares, per canton, which of its parameters are official and which are estimated, and distinguishes the written track from the oral one, because in several cantons the real assessment is an interview and presenting a multiple-choice simulation as "the exam" would be false.
In public. A transparency page, in four languages, states the service commitments: sources checked every twenty-four hours, ordinary refresh within seven days, corrections affecting correctness within seventy-two hours. It also offers cantonal naturalisation authorities, the federal migration secretariat and accredited researchers an inspection of the verification record on written request, answered within five working days.
Publishing a service commitment you can be measured against is uncomfortable. It is also the only version of transparency that means anything.
7. One developer, thirteen releases in eighteen weeks
The first commit is dated 12 April 2026. The app was in production on 2 May. Since then: thirteen public minor versions through to 3.12, 243 builds, and at least one build on 89 of the 142 days.
That pace is only survivable because of what sits underneath it: 186 automated test files, 320 versioned database migrations, and a content pipeline that verifies its own output rather than trusting it. The interface is held to four languages by a test that refuses to build if one locale has a key the others do not. Currently 3,426 keys, identical across all four.
None of this is heroic. It is the ordinary consequence of the fact that a single person cannot hold the state of twenty-six cantons in their head, and should not try.
8. The agents as users of the system
This is the chapter I did not expect to write.
Much of this project was built with AI coding agents. The interesting part is not that they wrote code. It is that they turned out to be users of the production system, and expensive ones.
On one day in August, the project's database egress broke down as follows: the tooling, meaning scripts and internal utilities and agents measuring things, produced roughly five times the traffic of every real user of the app combined. The app was never the problem. Measuring the app was. And it pushed the project against a hard billing threshold with a deadline.
The failure mode was specific and, in hindsight, obvious. An agent asked to check that a seed worked would read the table back. An agent asked how many rows existed would fetch the rows and count them. An agent asked whether something had changed would download it and compare. Every one of those is a reasonable instinct for a human at a terminal, and catastrophic at machine frequency.
What came out of it is a repository constitution, read automatically at the start of every agent session, whose first chapter is not about code style. It is: every query you run against this database is billed. Then the rules that follow from it. Never read rows in order to measure them, because a count and a size and a fingerprint are one row each. Never select every column from the heavy tables. Aggregate on the server and receive ten rows instead of ten thousand. One session per task, not one per command, because every new connection pays a fixed protocol cost before the query even runs. And verify a write with numbers, never with a dump.
Alongside it sits a persistent project memory, one fact per file, indexed, carrying the decisions an agent cannot re-derive from the code: which environments may be written to and under what conditions, which corrections were already tried and failed, which parts of the pipeline a fresh session must not touch alone.
The lesson generalises beyond this project. We are used to reasoning about an agent's cost in tokens. In a system with a real backend, the agent's cost is also the load it puts on your infrastructure, and that number can dwarf the first one. Nobody had told me to instrument it. The bill did.
9. What it actually costs
The infrastructure itself, backend and distribution and developer accounts, runs on the order of a few hundred Swiss francs a year. The dominant line is not infrastructure at all: it is the AI tooling that does most of the building, a subscription I would hold anyway and that is shared across everything I work on. Fully attributed, the whole operation sits around three thousand francs a year. Strictly attributed to this project, it is a fraction of that.
For that, at the time of writing: 10,010 registered profiles, of which 7,317 have actually studied or played, across twenty-five of the twenty-six cantons.
I put the numbers in because they are the part people find hardest to believe, and because they frame the real question. This is not a story about doing a lot with very little as a virtue. It is that a civic tool at national scale can now be built and maintained by one person, and that the binding constraint has moved. It is no longer engineering capacity. It is whether the institutions that own the content have a way to participate in keeping it correct, and that is a question no amount of code answers on its own.
