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-first

Decision

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).
The “no learning curve” of Dart in a vibe-coded workflow removes Flutter’s only real cost (language unfamiliarity), leaving its structural advantages decisive. Rejected alternatives:
  • 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.