Repository navigation
Update mobile/README.md for development builds — Expo Go no longer runs the app #129
Description
Description
ADR 0016 (lands with #128) makes development builds mandatory: useSQLCipher is a build-level flag, so SQLCipher requires npx expo prebuild and a dev build, and Expo Go can no longer run this app.
mobile/README.md was not updated to match, so the moment #128 merges the onboarding instructions tell contributors to do something that will fail:
mobile/README.md:13— lists Expo Go as a prerequisitemobile/README.md:31— "scan the QR code with Expo Go"mobile/README.md:53— "Expo Go still runs the app on a…"
0016 calls this out directly: "Contributor onboarding changes and the docs have to change with it… the first-run cost (a native build) needs saying up front rather than discovered." It also notes this lands on the same people 0008 designed a gentle first ticket for, so the setup instructions matter more here than usual.
ADR 0017 adds a second doc gap: the journal database refuses to open on web by design, so npm run web is the landing page and not a preview of the app. A contributor who runs it and sees something render will reasonably assume otherwise.
Scope is mobile/README.md and any sibling contributor docs — no code changes.
Acceptance Criteria
- Expo Go removed as a way to run the app; replaced with the
npx expo prebuild+ development-build path - First-run cost (a native build, and that it is slow the first time) stated up front, not buried
- Prerequisites corrected — Xcode/Android Studio requirements for a dev build, and the required Node version (
enginesis>=20.19.4) -
npm run webdocumented as the landing/marketing surface only, per 0017 — explicitly not an app preview - A contributor with a clean checkout can follow the README start to finish and reach a running app
- The feature/s being implemented are covered by unit tests - If not, create tests for them on this ticket (docs-only; no tests expected)
Additional Info and Resources
- Source: ADR 0016 (Consequences) and ADR 0017 (Consequences), both in Add encrypted storage and biometric unlock foundation #128
- 0016 also notes demoing to non-technical stakeholders no longer works via Expo Go — a shared development build or the web landing page is the substitute, worth a line in the README
- CI is unaffected (lint/typecheck/jest need no native build), so this is purely a local-development docs problem
QA
- On a clean clone, follow the README exactly on macOS (iOS) and confirm the app runs
- Repeat on Linux/Windows (Android) and confirm the instructions hold
- Confirm nothing in the README still implies Expo Go or
npm run webwill show the journal
Activity
Jberma23 commentedon Aug 16, 2026
I know you already documented some of these. But review:
Code review — PR 128, encrypted storage & biometric unlock foundation
Verification I ran (after npm ci — my checkout's node_modules was stale, the lockfile itself is correct):
┌──────────────────┬───────────────────────────────┐
│ Check │ Result │
├──────────────────┼───────────────────────────────┤
│ npx tsc --noEmit │ clean │
├──────────────────┼───────────────────────────────┤
│ npx expo lint │ clean │
├──────────────────┼───────────────────────────────┤
│ npx jest │ 10 suites, 95 tests, all pass │
└──────────────────┴───────────────────────────────┘
Overall this is unusually good work. The layering (key → database → migrations → repository, with the gate
separate) is right, the injectable get testable against real SQLite insteadof asserting on SQL strings, and ADR 0015's reasoning for rejecting requireAuthentication is correct and
important. Comments explain why rather what follows is about gaps betweenwhat the code intends and what it enforces.
src/lib/db/database.ts:53-58 keys the lite_master to "force SQLCipher toactually decrypt a page". That probe doesn't do what the comment says. I verified against plain SQLite:
PRAGMA key on plain SQLite: ACCEPTED SILENTLY (no error)
sqlite_master probe still works: {"c":
PRAGMA cipher_version -> null
SQLite ignores unknown pragmas rather than erroring, so on any binary without SQLCipher compiled in, PRAGMA key is a no-op, the probe passecrypted medical journal with no signalwhatsoever. That binary is easy to end up with: Expo Go (config plugins don't apply there, and the README still tells contributors to use it), arebuild that predates this change, or a cached EAS build.
This is precisely the silent downgrade database.ts:42 refuses to allow on web, and ADR 0017's "failing
loudly is better than a fallback that . The check is cheap:
const cipher = await db.getFirstAsync<PRAGMA cipher_version');
if (!cipher) {
throw new DatabaseUnavailableError(
'This build has no SQLCipher, so the journal would be stored unencrypted. Run
npx expo prebuildanduse a development build — Expo Go cann
);
}
Run it before PRAGMA key, and the erroarding hint for finding 2.
ADR 0016 lists this as a required consADME.md:13 lists Expo Go as aprerequisite, :31 says to scan the QR code, :53 says "Expo Go still runs the app on a physical iPhone
without a Mac." Combined with finding the README, gets a working app, andwrites plaintext. ADR 0008 designed this area as the gentle first ticket, so the setup docs matter more
than usual here.
repository.ts:146 and :164 call this.f
PROBE A: const { update } = contacts;
→ TypeError: _this2.find is not a function
const { update } = repo and onPress={repo.update} are both ordinary React usage, and the failure is a
runtime TypeError that types won't cation find(id) above the returned objectand call it directly from update.
repository.ts:136 spreads the caller's input rather than the row. Verified:
phone: null. So created ≠ find(created
["name","phone","notAColumn","id","cre
The existing test at repository.test.tis null, which passes while thereturned value is undefined. This will bite the first screen that renders created directly instead of
re-listing. Building the return value null) fixes both, andexpect(created).toEqual(await find(created.id)) is a good regression test.
database.ts:103 swallows every error from deleteDatabaseAsync. Looking at expo-sqlite's iOS
implementation, that call throws Deletabase is still in the module'sconnection cache and DatabaseNotFoundException if the file is absent. The blanket catch treats both the
same, so a genuinely failed deletion r it to the not-found case gets you thebehaviour the test at database.test.ts:159 claims to cover.
Related: closeJournalDatabase at database.ts:89 awaits db?.closeAsync() unguarded, so a close failure
rejects destroyJournalDatabase before ordering where the user keeps areadable journal after asking for deletion.
While there — the rationale at database.ts:96-99 and database.test.ts:153 doesn't parse: "an unreadable
database rather than a readable one withat can't exist, since a database with no key is by definition unreadable. Deleting the key first is the stronger order, because it guarantees
unreadability at the earliest moment els.
unlock-gate.tsx:105-109 sets {status:'less of the previous state. Verified:
PROBE D: no-lock device, Continue → ba
→ "Your journal is locked" / "Unlock with your phone to open it."
On a device with no lock there is nothing to unlock with. The user taps Unlock, gets the "Your phone has
no lock set" screen again, taps Continpps. Re-locking to no-device-lock whenthat's where the state came from, or re-checking availability before choosing the copy, keeps ADR 0018's
placeholder honest.
Same listener: background only. On iOSand the OS snapshots the screen for the switcher card before background arrives, so the card shows journal content and persists to disk. The
usual fix is an opaque cover rendered lock state, so it doesn't fight thebiometric prompt) plus the existing re-lock on background. Reasonable as a follow-up issue rather than in
this PR, but the comment at :99-101 cl so the gap should be recordedsomewhere.
Smaller things
so it can't currently fire — but the ftakes are the ones the file's ownheader describes. The same shared-in-flight-promise pattern used for openPromise would close it.
BiometricPrompt rejects a negative butAL. The copy the comment thought aboutis iOS-only; fallbackLabel is the iOS-side lever if you want more control.
something rather than failed.
One thing worth folding back into the
ADR 0015 says issue #115 "still has totabase file itself." That's now aniOS-only job. Adding expo-secure-store to plugins applies its backup rules with configureAndroidBackup defaulting true, and those rules use <ath="."/> as their only include — under Android's Auto Backup semantics that excludes everything else, including files/SQLite/journal.db. On iOS the database sits in Documents/SQLite/pted, with the key correctly excludedby THIS_DEVICE_ONLY, so a restore yields an unreadable file — the boundary 0015 intends). Scoping #115 to
NSURLIsExcludedFromBackupKey on iOS wog this.
What's good
The requireAuthentication analysis in the most valuable thing in this PR —it's a trap most implementations walk into, the reasoning is right, and key.test.ts:55 pins it with a test that explains itself. jest/in-memory-smigration rollback test atmigrations.test.ts:83 verifies real transactional behaviour rather than a mock's opinion, and it's honesabout what it can't cover. The empty Mserting it stays empty is a nice way to keep "foundation" and "first feature" separable. And ADR 0018 recording an unmade decision, rather than letting the implementation quietly becthe right instinct.
Findings 1 and 2 are the ones I'd hold and localised.
Document development builds, and stop `npm start` reaching for Expo Go