Research 0002 — React Native framework choice: Expo vs. Community CLI
Status: Decided
Date: July 2026
Author: Claude Code (session with the developer)
Scope: Task 0.1.1 / epic 0.1 — which React Native toolchain the repo scaffold is built on, and which parts of that toolchain's hosted services are deliberately excluded
Related: 0001 decided React Native as the client framework but did not pick a toolchain. Affects epic Infrastructure tasks 0.1–0.4 and epic Mobile App Shell.
Question
0001 committed to React Native for iOS + Android from Phase 1, but left the toolchain unstated. Which React Native framework should task 0.1.1 scaffold on — and does anything about a sovereignty-first, provably-offline product argue against the ecosystem's default answer?
Findings
The framework question is not the same question it was
The React Native core team no longer recommends bare react-native init for new projects; the docs direct new apps to start with a framework, and name Expo as the one they endorse. The historical objection to Expo for this project's shape — "on-device llama.cpp needs native code, so we need bare RN" — no longer holds:
llama.rnships an Expo config plugin. It takesenableEntitlements,forceCxx20, andenableOpenCLoptions for exactly the iOS/OpenCL native configuration this app needs. There is a known ESM resolution bug in plugin loading — a bug to work around, not an architectural wall.- Expo is no longer a one-way door.
expo prebuildgenerates realios/andandroid/directories. Committing them at any point yields Community-CLI-equivalent native control while keeping theexpo-*module library and config-plugin system — the escape hatch is always one command away, which is what makes adopting Expo low-risk rather than binding.
What Expo concretely buys this specific roadmap
- Task 0.4 (model asset pipeline). The epic names resumable download plus checksum verification as the fix for the stuck-download failure the developer hit evaluating OGAM.
expo-file-system'screateDownloadResumableis that primitive, already maintained and tested, rather than hand-rolledRNFScode. - Task 0.3 (native build tooling). Signing, capabilities, and the Tier 3 native-module placeholder become declarative config-plugin state instead of hand-edited Xcode project files that drift between machines.
- Connector credential storage (epics 2.2, 4.2).
expo-secure-storeis Keychain/Keystore-backed, matching 0001's "token stored in the OS keychain, scoped per-connector" decision.
Where Expo's defaults conflict with the product's trust claim
Two of Expo's headline features are actively wrong for this product, and both are opt-out rather than opt-in:
expo-updates(OTA JavaScript delivery) means the code actually running is not the code in the audited, store-reviewed binary. For an AGPL product whose entire proposition is verifiable local-only execution, a hot-swap channel undercuts task 1.5 / 0.1.12 (zero-network enforcement and audit) at the concept level, not merely at the packet level. It is also, unavoidably, a network callback in an app that claims to make none.- EAS Build is a hosted third-party build service that would hold the project's signing keys. For a sovereignty-branded project that is an avoidable custody question;
eas build --localor plain GitHub Actions produces the same artifacts without it.
The residual cost of choosing Expo is a larger dependency surface for 0.1.12's audit to cover. That audit is empirical either way — run the app under packet capture, assert zero egress outside a granted connector — so the cost is breadth of review, not a different kind of work.
Options considered
A. Expo, with continuous native generation (recommended)
Full expo-* module library and config plugins, no dependency on Expo's hosted services. Ecosystem-default tooling, which matters for a solo/small- team Phase 1.
ios/ and android/ stay generated, not committed. Committing them alongside prebuild is the worst of both worlds: prebuild overwrites hand-edits, so the native projects become a source of merge noise and silent drift — exactly the failure mode that choosing Expo was meant to avoid. With CNG, app.json plus config plugins are the single source of truth, which is what makes task 0.3's "declarative native config" benefit real. Committing them remains available later as a deliberate migration, not a default.
B. @react-native-community/cli ("bare" React Native, no framework)
Smallest dependency surface, which is the one thing it wins on. Costs hand-rolled equivalents of expo-file-system's resumable downloads and expo-secure-store, plus the well-known RN upgrade tax. Discouraged by React Native's own docs for new apps.
C. Rock (formerly React Native Enterprise Framework / rnef, by Callstack)
The only genuine third framework. Keeps bare native projects, adds a CLI, pluggable bundlers, out-of-tree platform support, and native build caching hosted on your own infrastructure via GitHub Actions — which maps directly onto the EAS key-custody objection above. Rejected for now on two grounds: it targets teams who have outgrown the Community CLI on large modular codebases, and Callstack themselves point new projects at Expo. Its self-hosted-build advantage is obtainable anyway via expo prebuild plus GitHub Actions.
Worth remembering as the migration target if Expo's dependency surface ever collides with 0.1.12. Migration is plausible precisely because both A and C leave real native projects on disk.
Not considered further
Ignite (Infinite Red) is frequently listed as an alternative but is a boilerplate generated on top of Expo or the Community CLI — orthogonal to this choice, layerable onto either.
Recommendation
Adopt Option A: Expo with prebuild, excluding expo-updates and EAS.
Leave expo-updates out of the template from the first commit. Retrofitting its absence later is harder than never adding it, and its presence would have to be explained away in every subsequent claim the product makes about offline verifiability.
Decisions
- Toolchain: Expo, using continuous native generation.
ios/andandroid/are gitignored and regenerated byexpo prebuild;app.jsonplus config plugins are the source of truth for native configuration. - Boot verification happens in CI, not on a developer laptop. The local machine can build neither platform — CocoaPods cannot be installed (Homebrew under
/opt/homebrewbelongs to a different macOS account; system Ruby is 2.6, below current CocoaPods' minimum), and no Android SDK or JDK exists. Rather than chase a per-laptop fix, epic 0.1's "boots on both platforms" check is satisfied by GitHub Actions runners, which ship CocoaPods, a JDK, and the Android SDK. This makes the check reproducible instead of dependent on one machine's setup, and it follows from the no-EAS decision above. - No
expo-updates. No over-the-air JavaScript delivery, ever. The shipped binary is the audited binary. - No EAS Build. Builds run locally or in GitHub Actions (task 0.2); the project holds its own signing keys.
- Expo Go is not a supported development path —
llama.rnrequires a development build, so iOS/Android dev builds are the only workflow. - Test runner: Jest via
jest-expo, a deliberate exception to the ecosystem's Vitest 4 standard rather than drift. React Native ships Flow types in its own source and resolves modules Metro-style; Vitest needs preset and transform workarounds to cope, and those break on RN upgrades.jest-expois maintained in lockstep with the SDK. The cost is one inconsistency withsovereign-desktop; the benefit is a test setup that survives SDK bumps without hand-holding.
Open questions
- Whether CI should also gate on a physical device. The simulator and emulator checks in
native.ymlcatch crash-on-startup, but not the device-only failures (memory pressure under a loaded GGUF model, thermal throttling) that matter most for on-device inference. Task 0.3 revisits this when real builds start going to TestFlight and the internal Play track. Whether the— resolved. Not present atllama.rnconfig-plugin ESM bug is still livellama.rn0.12.8: the plugin resolves andexpo prebuildcompletes with it active, and the Android build links:llama.rn:assembleDebugand packageslibrnllama*.sointo the APK. No patch needed.llama.rnships prebuilt native binaries rather than building from source. Itspostinstalldownloadsllama-rn-ios-xcframework.tar.gzandllama-rn-android-jni-libs.tar.gz, which are too large for npm. For a product whose claim is verifiable local execution, the inference engine's binaries arriving over the network at install time is a supply-chain link worth a deliberate decision — accept them, pin and checksum them, or build from source. Unresolved.- Debug APK size grew from 130 MB to 226 MB when
llama.rnwas added, as it ships a native library per CPU feature variant (v8, v8_2, dotprod, i8mm, hexagon, opencl) for both ABIs. A release build wants ABI splits and no x86_64 slice; revisit at task 0.3 or 8.2. - Local Android toolchain. Nothing is installed — no SDK, no
ANDROID_HOME, no Android Studio, noadb, and no Java runtime at all. Deferred deliberately: JDK plus the Android command-line tools unpack into$HOMEwithoutsudoor Homebrew, so this is unblocked whenever local Android work actually starts.
Next steps
Task 0.1.1 scaffolds on this decision. Task 0.2 (CI pipeline) inherits the "no EAS" constraint, must build on GitHub Actions runners, and now also carries 0.1's boot check — 0.1.1 stays open until CI proves the app launches on both platforms. Task 0.1.12 (zero-network enforcement and audit) should treat this doc's dependency- surface caveat as a named input — the audit's scope includes every expo-* package the app ends up shipping.