
Mobile Application
A mobile-only, local-first application presenting religious and scientific content for every week of pregnancy, with source references. The content ships with the app and is written into the device's SQLite in a single transaction; the core flow works without a connection and no personal data is sent to a server.
The content this application carries is sensitive in two ways: one side is religious reference, the other health information. In both, the cost of a wrong sentence differs from the cost of a software bug — the user acts on the assumption that the information is correct. So the real technical problem is not the screen but how content reaches release: a mechanism was needed to stop unreviewed text from slipping into a version. The second issue is privacy: pregnancy data is among the most sensitive a person has. "Keep it on a server, encrypt it later" is the wrong answer here; the right answer is not to collect it at all.
One type of user but a variable context of use: the app is opened in short bursts through the day, mostly at home, sometimes without a connection, and most likely on the single device it was installed on. Those three conditions together make startup speed and offline operation a requirement rather than a preference. There is a second party, but not inside the app: the person reviewing the content. Their decision lives in the repository as a field.
Single developer. Content schema and validation pipeline, device database layer and import logic, pregnancy-week domain logic, interface and release gate.
The system is serverless: there is no API the app talks to. Content lives in the repository as week-by-week JSON defined by a Zod schema; when the app is packaged, that content goes inside the binary. On first launch the content is imported into the device's SQLite database. The import carries three properties at once: versioned — a unit is not overwritten unless its content version increases; idempotent — running the same import a second time changes nothing; and single-transaction — it never leaves a half-finished import behind. Large imports are split into chunks so the interface does not lock up, each chunk runs in its own transaction, and control is yielded between chunks. The application layers are separated: pure domain functions (pregnancy-week calculation, week access control), the database layer, the content schema and the routes. The week calculation is timezone-independent — because it is computed from a day difference, changing the device's zone does not skip a week.
Pregnancy data is sensitive; data that is not collected cannot be leaked. Since the content ships with the app anyway, a server's only job would be to synchronise personal state — and the cost of that single feature is an account system, a privacy obligation and a permanent operating expense. That trade was not accepted.
Trade-offWhen a user changes device their progress does not travel, and deleting the app loses the data; there is no backup. Content corrections do not come from a server either: getting a typo to the user requires a new store release.
The app can call the import on every launch, and the same path runs after an update. So how many times the import runs must not change the result. The comparison is made on the content version: if the version has not increased, the row is not touched. On an update, only changed units are written.
Trade-offThe guarantee depends on the content version being incremented correctly; changing a text and forgetting to raise the version means the change never reaches the device — a silent failure. Nothing forces the version to increase.
Schema validation tells you a field is filled, not that it is correct. For religious and medical content, the second is what matters. So every content block carries a review status, and the gate that runs before a release lists the files and exits non-zero if it finds an unapproved block — the build stops.
Trade-offThe gate only looks at the value of a field; the correctness of the approval itself depends on a human. There is no mechanism catching a text approved by mistake, only one catching text that was never reviewed. And a release cannot be built while working with draft content.
On both the religious and the scientific side, the source of a claim matters as much as the text itself. Once the source is defined as a schema field rather than a habit, text without a source cannot enter the repository. A separate command checks that the links still resolve.
Trade-offThe link check goes over the network and depends on external sites being reachable; a temporary outage makes the check look failed. That is why the link check is not part of the release gate but a separately run command.
Expo lets a single developer deliver to both platforms and is the lowest-cost path in a scope that does not require writing native modules. SQLite was chosen as the device database because the content has to be relational and queryable; in a key-value store you cannot write a query across week, layer and language. The synchronous database interface keeps read paths simple without asynchronous state management. Zod acts as both the content schema and the type source — one schema, two jobs, as in MedAtlas. i18next lets both languages ship with the content; translations are embedded in the app rather than fetched from the network.
The attack surface is deliberately narrow: there is no server the app talks to, no account system and no authentication layer. Personal state — week, progress, preferences — lives only in the device database and is never sent anywhere. Because the content ships with the app, there is no payload downloaded at runtime that would need verifying. The only remaining risk is the content itself, and that is guarded by the release gate: unapproved content cannot enter a version.
This is a mobile application; a page-speed measurement is meaningless, and because there is no stored startup-time measurement, no number is written here. Structurally there are two decisions. First: because the content ships with the app, no download is awaited on first launch — the screen is full even without a connection. Second: the content import is split into chunks so the main thread is not blocked, each chunk runs in its own transaction and control is yielded between chunks; the interface does not freeze on first launch.
Tying the release gate to content approval was the most right decision I made in this project, and its origin is MedAtlas: there the gate asked "does it match the schema", and for educational content that question was not enough. Here the question became "has it been reviewed". What is missing is the version number itself: the idempotency of the import depends on the content version being incremented correctly, and nothing enforces that — changing a text and forgetting the version means the change never reaches the device. Today I would write a check that hashes the content file and compares it against the version, so the rule would live inside the gate rather than in someone's attention. It is the same thing I learned in Bergaz Operations: as long as a guarantee lives in a habit rather than in a check, sooner or later someone forgets.
Tell me what you want to build; I'll tell you up front how long it takes and where to start.