ADR-0037: Flutter for the mobile client (over React Native, Capacitor, native)
Status
Accepted (pending FieldForce Phase 1 implementation)Tags
fieldforce, digital-worker, mobile, flutter, platform, vibe-coding, offline-firstDecision
The Gremlin mobile apps (FieldForce first, Digital Worker second) are built in Flutter, targeting Android and iOS from one codebase, rather than React Native, Capacitor, or native Kotlin/Swift. The mobile apps are clients of the existing Go/Postgres/NATS backend. Flutter is the UI/runtime layer only — it introduces no second source of truth (see ADR-0038 monorepo, ADR-0039 PowerSync).Why
The deciding context is a fully LLM-driven (“vibe coding”) workflow with two hard product requirements — critical offline-first and realtime updates — and a stated priority of fewest bugs and least debugging time, with best performance/UX as the next priority. Flutter wins on the axes that dominate under those constraints:- Cohesive, first-party ecosystem → generated code is internally consistent; the LLM cannot pull a dead or version-incompatible library, which is the largest source of mystery bugs in generated mobile code. This directly serves the “least debugging” priority.
- Sound null safety catches a class of bugs at compile time — closing the gap between “looks right” and “runs right” that is most dangerous in generated code.
- Own rendering engine (Impeller) → pixel-consistent UI across devices and platforms, eliminating “works on my device, breaks on yours” defects, and best raw UX.
- Unified tooling (DevTools,
flutter test, hot reload) → tighter, less brittle dev/test/build loop. - Pixel-accurate Cupertino + Material both ship in-box, so a single toolkit can present native-feeling components per platform while carrying a custom brand (see ADR-0046… brand wrappers in CLAUDE.md).
- React Native + Expo. Strong on language consolidation (TypeScript shared with the backend monorepo) and OTA via EAS — the best strategic fit. Rejected because its ecosystem fragmentation produces more generated-code bugs, its offline-sync story is hand-assembled, and its dev-cycle cohesion is weaker — all in direct conflict with the “least debugging” priority. The shared-types win is a strategic nicety that does not reduce per-feature debugging or shipping time, which were the ranked priorities.
- Capacitor (web shell). Fastest reuse of the existing web UI, but a WebView is the worst fit for critical offline-first plus best UX. Rejected — it fails both hard requirements.
- Native Kotlin + Swift. Best platform integration, but two codebases, two languages, slowest to ship; unjustified for a solo/lead vibe-coded build. Rejected for MVP.
Consequences
- Dart is a separate language island from the TS monorepo — no client/backend type sharing. Shared contracts (DTOs, validation) are re-expressed in Dart; drift risk is mitigated by keeping the Go backend authoritative and the client thin.
- iOS builds require macOS — the homelab ARM cluster cannot build iOS, forcing a cloud-mac CI path (see ADR-0045 Codemagic).
- Flutter lacks first-class OTA code push; Shorebird is the later option if needed (deferred).
- Heavy reliance on codegen (
build_runner) — accepted as discipline (CLAUDE.md §15). - A larger baseline app size (engine included) — acceptable for the use case.
Rules for agents
- Target Flutter (Android + iOS) from a single codebase. The mobile app is a client; the Go backend stays the source of truth.
- Prefer first-party / cohesive solutions over long-tail third-party packages to keep generated code consistent.
- Lean on null safety and sealed types for compile-time safety (see ADR-0040 Result).
- Do NOT introduce React Native, Capacitor, or native modules as parallel paths.