Overview
OffRead is a privacy-first reading tracker and PDF reader for Android and iOS. The premise is radical simplicity: no account, no cloud, no tracking. Everything lives on the device.
The client came to us with a clear vision and a tight timeline. We shipped from zero to Google Play internal testing in a single sprint.
"Your reading life. Tracked offline." — the entire product philosophy in five words.
The Challenge
Most reading tracker apps are cloud-dependent. They require sign-in, sync user data to remote servers, and break when offline. For privacy-conscious readers — particularly those in regions with unreliable connectivity — this is a non-starter.
Our challenge was threefold:
- Build a fully offline architecture with no network dependency whatsoever
- Ship a custom PDF reader that didn't crash on Android (spoiler: the popular libraries did)
- Deliver to production in one sprint with a monorepo setup ready for future growth
System Architecture
graph TD
UI[React Native UI<br/>NativeWind + Expo Router]
ZS[Zustand<br/>Client State]
TQ[TanStack Query<br/>Server State Layer]
DRM[Drizzle ORM<br/>Type-safe Queries]
DB[(SQLite<br/>expo-sqlite)]
PDF[PDF.js Renderer<br/>react-native-webview]
FS[expo-file-system<br/>Local File Storage]
UI --> ZS
UI --> TQ
TQ --> DRM
DRM --> DB
UI --> PDF
PDF --> FS
FS --> DB
style UI fill:#1A1A2E,stroke:#7DFF00,color:#fff
style DB fill:#0D0D18,stroke:#7DFF00,color:#fff
style PDF fill:#1A1A2E,stroke:#7DFF00,color:#fff
The key architectural decision: SQLite as the single source of truth. Every book, session, note, and streak lives in a local SQLite database accessed via Drizzle ORM for type-safe queries and automatic schema migrations.
Tech Stack
| Layer | Choice | Why |
|---|---|---|
| Framework | Expo + Expo Router | File-based routing, EAS Build pipeline |
| Language | TypeScript | End-to-end type safety |
| Database | SQLite via expo-sqlite |
True offline, no sync needed |
| ORM | Drizzle ORM | Type-safe queries + migrations |
| State (client) | Zustand | Lightweight, no boilerplate |
| State (server) | TanStack Query | Consistent data fetching patterns |
| Styling | NativeWind | Tailwind CSS syntax on native |
react-native-webview + PDF.js |
Custom renderer (see below) | |
| File I/O | expo-document-picker + expo-file-system |
Cross-platform file access |
| Build | EAS Build | AAB for Play Store, IPA pipeline |
The PDF Renderer Problem
This was the hardest technical challenge on the project.
Every popular React Native PDF library we evaluated — react-native-pdf, others — crashed on Android with certain PDF structures. The crash was non-deterministic and impossible to fully reproduce in development.
Our solution: build a custom renderer from scratch using react-native-webview + PDF.js.
sequenceDiagram
participant User
participant RN as React Native
participant WV as WebView
participant PDFJS as PDF.js
participant FS as File System
User->>RN: Open PDF
RN->>FS: Read file URI
FS-->>RN: Base64 encoded PDF
RN->>WV: Inject PDF data + PDF.js
WV->>PDFJS: Render page
PDFJS-->>WV: Canvas output
WV-->>User: Rendered page
User->>WV: Swipe / page jump
WV->>PDFJS: Render next page
The renderer supports:
- Swipe navigation between pages
- Page jump input
- Memory-optimized rendering (only adjacent pages stay in memory)
- Consistent behavior across Android API levels
Core Features
Reading Timer — Users start timed sessions and log pages read. All session data (start time, duration, pages) is persisted locally.
Book Library — Search books (title, author, ISBN), add them with reading status: Want to Read, Currently Reading, Finished.
Session Notes — Rich notes anchored to a page range, written during or after a reading session.
Reading Streaks & Stats — Daily goal tracking, streak counters, and reading history visualizations — all derived from local session data.
Offline-First Architecture Details
The app has zero network calls. This required careful architecture:
- Schema migrations: Drizzle ORM handles SQLite schema migrations automatically on app launch, so updates never break existing user data
- File management: PDFs are copied into the app's sandboxed document directory via
expo-file-system, so they persist across app restarts - No background sync: No sync workers, no queue, no retry logic — because there's nothing to sync
Monorepo Setup
The project uses a Bun-powered monorepo with shared tooling:
offread/
apps/
mobile/ ← Expo app
packages/
db/ ← Drizzle schema + migrations (shared)
ui/ ← Shared NativeWind components
This structure means future surfaces (e.g., a web companion) can share the schema package directly.
Outcome
- Shipped to Google Play internal testing from zero in a single sprint
- 100% offline — no network permission required
- Custom PDF renderer with zero crashes in testing across 10+ Android devices
- EAS Build pipeline configured for both AAB (Android) and IPA (iOS)
Want to build a production-ready mobile app with a tight timeline? Book a free discovery call →