Skip to main content
Back to Projects
Rahmina logo

Rahmina

Mobile Application

Rahmina architecture diagram Rahmina Local-first mobil — içerik uygulamayla gelir, kişisel veri cihazdan çıkmaz Expo SDK 57 · React Native · expo-sqlite Sunucu yok — bu bir eksiklik değil, karar. DERLEME ÖNCESİ İçerik kaynağı hafta hafta JSON · TR ve EN Zod şeması · kaynak referansı zorunlu her blokta review durumu: draft | approved Yayın kapısı content:check:release onaysız tek blok varsa dosyaları listeler, sıfırdan farklı çıkar dini ve tıbbi içerik onaysız yayına çıkamaz Uygulama paketi içerik ikili dosyanın içinde indirme adımı yok ilk açılışta içerik zaten orada CİHAZDA expo-sqlite — cihazdaki tek gerçek kaynak Sürümlü import contentVersion artmadıkça yazmaz aynı sürüm ikinci kez atlanır idempotent Tek transaction yarım kalan import yok parçalara bölünür, arada kontrol devredilir arayüz donmaz Kişisel durum hafta, ilerleme, tercihler yalnız cihazda sunucuya gönderilmez Kazanç Çekirdek akış internetsiz çalışır. Sunucu maliyeti yok, sızdırılacak kullanıcı veritabanı yok, gizlilik metni "veri toplamıyoruz" diyebiliyor. Bedeli İçerik düzeltmesi mağaza güncellemesi gerektiriyor; bir yazım hatası bile sürüm çıkmadan kullanıcıya ulaşmıyor. Cihaz değişince ilerleme taşınmıyor.
A local-first Expo / React Native app with no server. Content is authored week by week as JSON, validated with Zod, and the release gate stops the deployment if even one block is unapproved. Content ships inside the app bundle — there is no download step. On device, expo-sqlite is the single source of truth; the import is versioned, idempotent and runs in one transaction. Personal state never leaves the device.

A mobile app offering religious and scientific content for every week of pregnancy, with sources. It works offline; personal data stays on the phone and is never sent to a server.

Technologies Used

  • Expo SDK 57
  • React Native
  • TypeScript
  • expo-sqlite
  • expo-router
  • Zod
  • i18next
  • Jest

Key Features

  • Local-first — the content is embedded in the app, there is no download step
  • Versioned content import into the device database that completes in one step and gives the same result if run again
  • The core flow works offline
  • No personal data is sent to a server — there is no account and no sync
  • A release cannot be built with unapproved content: the gate stops the build
  • Two languages (TR/EN) and a timezone-independent week calculation

Development Time

1.5 months

Problem

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.

Users

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.

My Role

Single developer. Content schema and validation pipeline, device database layer and import logic, pregnancy-week domain logic, interface and release gate.

Architecture

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.

Key Decisions

  1. Local-first — no server, no account, no sync

    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.

  2. The content import is versioned and idempotent

    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.

  3. The release gate looks at approval, not at the schema

    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.

  4. The source reference is a required field in the content schema

    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.

Stack Rationale

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.

Security

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.

Performance

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.

Results

Active development
2026-07 · 31 commits
TypeScript source files
80
Test files
14 — import, domain logic and content schema
Content
Two languages (TR/EN), week-based JSON units; a required source reference and review status in every block
Architectural guarantee
Personal data never leaves the device — there is no server the app talks to
Architectural guarantee
The import is idempotent: the same version is not written twice and no half-finished import is left behind
Architectural guarantee
A single unapproved content block stops the release build

What I Would Do Differently

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.

Prototype

Let's find where to start

Tell me what you want to build; I'll tell you up front how long it takes and what the sensible first step is.