An Android distraction-blocker with a configurable daily-habit gate.
DoomGuard sits between a user and the apps that steal their attention. A custom Kotlin Expo module runs a foreground service that polls Android's UsageStatsManager.queryEvents() to detect the current foreground app; when a user-configured blocked app launches while today's habit is incomplete, a full-screen SYSTEM_ALERT_WINDOW overlay drops in over the offending app and routes the user back into DoomGuard. The same gate state controls an embedded WebView — Instagram is rendered locked to /direct/inbox/ (DMs only) until the habit clears, then unlocks to the full feed with Reels and Explore surgically removed via injected JS.
The habit DoomGuard ships with is daily Bible reading: an adaptive plan generator splits the 31,102-verse canon across however many days the user picks (balancing daily volume by per-chapter word count, not chapter count); NIV, ESV, and NKJV are bundled as JSON for offline reading; an optional BYO-key AI assistant (Anthropic, OpenAI, or Gemini) provides streaming chat and study commentary with tap-to-jump verse citations. The architecture is habit-agnostic — Bible reading is the example, not the thesis.
Built solo in TypeScript on Expo with a custom Kotlin Expo module, patch-package'd native WebView, and no backend (all state is on-device in AsyncStorage). For the full technical tour — color palette, navigation graph, dual-plan architecture, every implementation choice — see CLAUDE.md.
- Two-mode Instagram WebView. Before today's reading: locked to
/direct/inbox/. After: full Instagram with Reels, Explore, and the "Use the app" upsell stripped via injected JS. The X-Requested-With header that Instagram uses to fingerprint embedded browsers is removed via apatch-packagepatch toreact-native-webview. - Native app blocker. A Kotlin foreground service polls
UsageStatsManager.queryEvents()every second; when a blocked package (Instagram by default — configurable from Settings against the device's launchable apps) comes to the foreground while today's reading is incomplete, a full-screenSYSTEM_ALERT_WINDOWoverlay drops in with a rotating Bible quote and a button back into DoomGuard. Survives reboot via aBOOT_COMPLETEDreceiver. - Dynamic reading plans. A verse-count-balanced plan generator splits the whole Bible across N days of your choosing (6 months, 1 year, 2 years, or custom), with three modes:
- On track — start clean, finish in N days.
- Catching up — pick the last chapter you read and a target end date; an expedited "catch-up" plan runs in parallel with the standard 365-day plan so the progress bar reflects real-Bible coverage, not catch-up coverage.
- Just read — chapter of the day or verse of the day, no commitment.
- Multi-provider AI study assistant (optional, BYO key). Choose Anthropic, OpenAI, or Gemini in Settings; the key never leaves the device except to the provider you picked. Two surfaces:
- Study Notes on any chapter — themes, historical context, application.
- Bible Chat — full conversational interface with streaming responses and
[[Book Chapter:Verse]]citations that tap through to the Reading screen.
- Offline Bible. NIV, ESV, and NKJV are bundled as JSON for instant cold-start reading. Other public-domain translations (KJV, BSB, WEB, ASV, BBE, YLT) fetch on demand from the wldeh CDN and cache in AsyncStorage. An opt-in background prefetcher can pre-download every chapter from bolls.life to keep cached copies fresh.
- Streak tracking, partial-chapter progress, mid-day reset hour (so late-night reading counts for the prior day), red-letter words of Jesus, Noto Serif typesetting with inline paragraphs and section headings.
| Layer | Choice |
|---|---|
| Framework | Expo SDK 55 (managed → prebuild), React Native 0.83, React 19 |
| Language | TypeScript (strict) |
| Navigation | @react-navigation/native + native-stack + bottom-tabs |
| Lists | @shopify/flash-list for infinite-scroll Bible reading |
| Native module | Custom Expo Module in Kotlin (modules/usage-monitor) |
| WebView | react-native-webview 13.16 + patch-package for header strip |
| Storage | @react-native-async-storage/async-storage |
| Bible data | Bundled JSON (NIV/ESV/NKJV) + wldeh CDN + bolls.life API |
| AI | Anthropic Messages, OpenAI Chat Completions, Gemini — streaming SSE |
| Fonts | @expo-google-fonts/noto-serif |
| Tests | Jest via jest-expo |
| Target | Android only |
DoomGuard is built around a small domain layer that screens, hooks, and services depend on but never bypass. The UI never news-up a service class — everything flows through src/services/factory.ts, which returns singleton instances.
┌─────────────────────────────────────────────────────────────────────┐
│ Screens / Components │
│ (Today · Bible · Instagram · Reading · Settings · Chat overlay) │
└──────────────────────────────┬──────────────────────────────────────┘
│ hooks only
┌──────────────────────────────▼──────────────────────────────────────┐
│ Hooks │
│ useBibleReading · useScripture · useChat · useAIEnabled · │
│ useAppBlocker · useStreak · useFontSize · usePlanData · ... │
└──────────────────────────────┬──────────────────────────────────────┘
│ factory singletons
┌──────────────────────────────▼──────────────────────────────────────┐
│ Services (behind domain interfaces) │
│ LocalReadingRepository · BibleAPIClient · AICommentaryService · │
│ AIChatService · StreakTracker · ScripturePrefetcher │
└──────────────────────────────┬──────────────────────────────────────┘
│
┌──────────────────────┼──────────────────────────────┐
▼ ▼ ▼
AsyncStorage wldeh CDN + Native UsageMonitor
(persistence) bolls.life API + Expo Module (Kotlin):
provider APIs foreground service,
boot receiver,
overlay window
Hooks that need cross-component sync (useFontSize, useReadingMode, useAppBlocker, useChat, useAIEnabled) use a module-level Set<Listener> pattern instead of Context — the same pattern, four lines, with AsyncStorage persistence.
CLAUDE.md is the internal technical spec — full file inventory, dual-plan architecture, every implementation choice and why.
- Node.js 20+ and npm
- Android Studio with platform-tools, an SDK platform ≥ 35, and the NDK
- JDK 17
- A connected Android device with USB debugging, or an emulator
- (Optional) An Expo account if you want to do cloud builds via EAS
git clone https://github.com/jayclim/DoomGuard.git
cd DoomGuard
npm install # runs patch-package via postinstall — important
npx expo prebuild --clean # generates ios/ and android/ from app.json
npx expo run:android # builds and installs the dev clientThe first build takes a while because the custom Expo module compiles Kotlin and the WebView header patch has to apply. Subsequent builds are fast.
To run the app blocker, after install:
- Open Settings → App Blocker in the app and tap "Grant permission".
- Toggle Instagram (or pick other apps from the dropdown).
- Trigger DoomGuard by opening Instagram before completing today's reading.
The blocker needs PACKAGE_USAGE_STATS (a "Usage Access" toggle in Android system settings — granted manually, no API for it) and SYSTEM_ALERT_WINDOW (Display over other apps).
app.json ships without an owner or extra.eas.projectId — those are personal to my account. To build via EAS:
npx eas init # creates a new EAS project tied to your Expo account
npx eas build --platform androidThis will write owner and extra.eas.projectId back into app.json automatically.
Tap Settings → AI Assistant, toggle it on, pick a provider, and paste your API key. The key is stored only in this device's AsyncStorage and sent only to the provider you picked. When AI is off (or no key is set for the active provider), every AI surface in the app — the Study Notes button, the Bible Chat overlay, long-press verse explainers — is invisible, not greyed out.
Supported providers:
| Provider | Get a key |
|---|---|
| Anthropic | https://console.anthropic.com/ |
| OpenAI | https://platform.openai.com/api-keys |
| Gemini | https://aistudio.google.com/apikey |
.
├── App.tsx # bootstrap
├── CLAUDE.md # full technical spec — read this for depth
├── modules/usage-monitor/ # custom Expo Module (Kotlin) for app blocker
├── patches/ # patch-package patches (react-native-webview)
├── src/
│ ├── components/ # VerseList, ChatOverlay, ChatButton, ...
│ ├── data/ # reading plan generator, bundled Bibles, red-letter data
│ ├── domain/ # pure interfaces and models — no React, no Expo
│ ├── hooks/ # useScripture, useChat, useAppBlocker, ...
│ ├── navigation/ # root stack + bottom tabs + unified tab bar
│ ├── screens/ # Today, Bible, Instagram, Reading, Settings, ...
│ ├── services/ # singleton factories behind the domain interfaces
│ │ ├── chat/ # multi-provider streaming chat
│ │ ├── commentary/ # multi-provider AI commentary
│ │ ├── local/ # AsyncStorage repository + streak tracker
│ │ └── scripture/ # dual-source Bible client + prefetcher
│ ├── storage/ # AsyncStorage key constants + preload cache
│ ├── utils/ # citationParser
│ └── webview/ # instagram-inject.ts — JS injected into the IG WebView
└── src/__tests__/ # jest tests
npm test # jest, 78 tests
npm run typecheck # tsc --noEmit, strict modeCI runs both on every push and pull request — see .github/workflows/ci.yml.
- Android only. iOS has no equivalent to
UsageStatsManager, so the blocker is fundamentally impossible to port. The WebView and reader would work on iOS but are not built. - Accountability, not a hard lock. A determined user can disable the foreground service, revoke the Usage Access permission, or uninstall DoomGuard. The design is to make those steps friction, not impossible.
- The Usage Access permission cannot be granted programmatically — the user has to flip a toggle in Android system settings. The app deep-links them there.
- Foreground-service notification is permanent while the blocker is active. Polling at 1 Hz is lightweight but does add a battery cost; switching to event-driven
UsageEventssubscriptions is on the roadmap.
- wldeh/bible-api — public-domain Bible translations as JSON over jsDelivr.
- bolls.life — modern Bible API serving NIV, ESV, NKJV with HTML-formatted verses, paragraph breaks, and section headings.
- OurManna — Verse of the Day API.
- The Expo and React Native teams.
MIT © 2026 Jayden Lim


