← All posts
Case Study Custom Mobile App Development React Native Expo SQLite Offline-First

Building OffRead: An Offline-First React Native PDF Reader & Reading Tracker

February 28, 2026· 8 min read

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:

  1. Build a fully offline architecture with no network dependency whatsoever
  2. Ship a custom PDF reader that didn't crash on Android (spoiler: the popular libraries did)
  3. 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
PDF 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:

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:

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


Want to build a production-ready mobile app with a tight timeline? Book a free discovery call →

Share this post

Want to build something like this?

15 minutes. We'll look at your business and scope what an AI automation built for it would actually do — concrete, no pitch deck.

Follow OffLift