Mobile engineering
Shia Library — Mobile App
The same library, offline-first on iOS and Android — hand-written Swift and Kotlin keep downloads alive under each OS's rules, and the offline store makes a half-visible book impossible. In external beta.
- 3,400+
- automated tests (Vitest)
- iOS + Android
- external beta, one codebase
- v13
- SQLite schema, forward-only
- ~830
- lines of hand-written Swift & Kotlin
The domain
This is the mobile surface of the Shia Library platform — one product, two clients — distributed as "Shia Library" on Google Play and "Shia Library - Books & Hadith" on iOS. It is currently in external beta via TestFlight and Google Play closed testing, with both platform builds cut from the same source commit; it is not yet on the public stores. The web, search, and backend story lives in the main Shia Library case study.
01 · Problem
The problem
Reading is where a library app actually gets used, and much of it happens offline. A book that arrives half-materialized isn't an inconvenience — in this domain, a silently missing chapter is a correctness failure.
Both operating systems actively kill long-running background work, each under a different and shifting contract — iOS background-processing budgets on one side, Android foreground-service types and timeouts on the other. A JavaScript-only download manager cannot survive process death; this is exactly where React Native alone runs out.
Search has to match the web's Arabic normalization exactly — a query that finds a passage online must find it offline — even with no network. And a beta still deserves production discipline: privacy-safe crash reporting, forward-only migrations, and store-policy compliance built before any public release exists.
02 · Approach
Approach & key decisions
A dual-authority offline store: intent vs projection
User intent — which books should be on this device — lives only in MMKV, the single writer. SQLite holds a one-way projection of downloaded content that can always be discarded and rebuilt, never trusted to write back. Every materialization is transactional and fenced by a corpus generation ID, so a crashed download, an account switch, or a corpus reset can never leave a half-visible book or resurrect stale content. Library metadata stays current through the backend's versioned change feed.
Swift and Kotlin where the OS demands it
Long downloads survive backgrounding through a custom Expo native module: ~380 lines of Swift managing iOS continued-processing tasks with a background-task fallback, and ~300 lines of Kotlin running an Android data-sync foreground service with timeout handling and aggregated multi-download notifications — plus roughly 150 more lines of Swift for App Intents (Siri) and a WidgetKit home-screen widget. The contract is deliberately narrow and fails closed: native code can extend work the user started, but can never originate a download, choose a book, or declare one complete.
Forward-only migrations, treated like a server database
On-device schemas persist for years on phones that skip app updates, so the SQLite store migrates forward only — 13 schema versions so far, with acceptance suites on the riskiest migrations plus tests that a failed migration rolls back cleanly, and a rule that a shipped migration is never edited, only appended to. Downloaded content survives migration or is invalidated atomically — never reinterpreted.
Search that degrades honestly, with Arabic parity
One search interface, three tiers: the shared backend's deterministic search RPC when online (semantic search stays web-only for now), SQLite FTS5 over downloaded content when offline, and an explicitly bounded approximate scan as a last resort. Arabic normalization is parity-locked to the server's SQL function and enforced by golden tests, so an offline hit highlights exactly what the web would. On a 21-case offline evaluation, retrieval scored Recall@10 = 1.0.
Privacy-first observability that earns its keep
Crash reporting is deny-by-default: scrubbers strip user, device, and request context, keep only bounded stack-frame basenames, and anchor trace propagation to a single origin — verified end-to-end on a shipped beta build, symbolicated native frames included. That telemetry has already paid for itself: it separated a native text-layout stall on very long Arabic chapters from an unrelated JavaScript issue, down to the exact rendering call.
Release engineering built before the release
Builds are traced by source commit, runtime fingerprint, and artifact hash across three channels, with an over-the-air kill-switch path held in reserve. Store-policy gates were cleared before any public listing exists: Android's foreground-service declaration, permission minimization verified on the shipped artifact, an iOS privacy manifest, and an account-deletion contract that meets both stores' policies. CI's last step scans the compiled bundle for secrets and leftover debug markers.
03 · Architecture
How it fits together
Offline store — dual authority
Native continuation — fails closed
Search — one interface, three tiers
Observability & release
04 · Results
Results
- In external beta via TestFlight and Google Play closed testing — both platform builds cut from the same source commit, with release provenance recorded for every artifact.
- 3,400+ automated tests (Vitest) with test code roughly matching product code line-for-line, plus 10 Maestro end-to-end device flows and physical-device regression matrices on both platforms.
- Measured on device under a documented protocol: steady-state list-scroll jank at a 0.25% median (target ≤5%) and time-to-initial-display at a 526 ms median — indicative numbers from a fixed test setup, not universal guarantees.
- Root-caused a device-only freeze — a collapsing header re-entering its own snap-settle path over 236,000 times — by instrumenting scroll events on hardware; fixed it with an in-flight latch and locked it in with a dedicated end-to-end regression flow.
- Architecture is documented like a team project: 58 architecture decision records cover everything from the offline store's authority model to the native background-download contract.
05 · Tradeoffs
Honest limitations
- This is a beta, not a launch: distribution is external TestFlight and Google Play closed testing only, and there is no public store listing yet. Getting there is the roadmap, not a formality.
- Content-integrity checks are structural, not cryptographic — materialization verifies gap-free chapter chains, counts, and the corpus generation ID, not signed hashes. A hostile CDN is out of scope for now.
- Known performance work remains on very long Arabic chapters: the diagnosis is precise — synchronous native text layout on oversized single nodes — but the fix hasn't shipped yet.
- The offline search tiers are deliberately unequal: FTS5 over downloaded content and the bounded approximate tier trade recall for latency and battery. Parity with the web is in the normalization, not the ranking.
- The native surface is deliberately small — ~830 lines where the OS contract demands it. This is a React Native app with native muscles where they matter, not a from-scratch native app, and I present it as exactly that.
06 · Next
What I'd do next
- Take the app from external beta to public App Store and Google Play listings — the store-policy gates are already cleared, so the remaining work is beta soak, feedback, and the final round of device-level fixes.
- Land the long-chapter text-rendering fix the diagnosis points at, validated under the same on-device evidence protocol as every other performance change.