Skip to content
← Work

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.

Solo — offline architecture, native modules, release engineering2026In progress6 min read
React Native (Expo)TypeScriptSwiftKotlinSQLiteMMKVSentryMaestro
Web & search platform case studySource private · walkthrough on request
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

Shia Library — mobile app · offline-firstArchitecture
React Native app
Expo Router · Hermes
Reader
AR · EN · offline
Downloads
user-started · resumable

Offline store — dual authority

Intent · MMKV
the only writer of user intent
Projection · SQLite
schema v13 · forward-only migrations
Atomic materialization
all-or-nothing per chapter

Native continuation — fails closed

Swift · background tasks
iOS continued processing + fallback
Kotlin · foreground service
Android data-sync · timeout-aware

Search — one interface, three tiers

Server search
shared deterministic RPC
Offline FTS5
parity-locked Arabic normalization
Bounded approximate
explicit last resort
Supabase · shared backend
same Postgres + RLS as the web platform

Observability & release

Privacy-first Sentry
deny-by-default scrubbing
CI bundle verifier
secret + debug-marker scan
Staged releases
TestFlight · Play closed testing
User intent lives only in MMKV; SQLite is a one-way projection of downloaded content, rebuilt atomically so a crashed download can never leave a half-visible book. Native Swift/Kotlin modules hold OS execution leases to keep user-started downloads alive — they can extend work the app asked for, never originate it. Search degrades tier by tier from the shared backend to on-device FTS5, with Arabic normalization parity-locked to the server.

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.