ADR-0038: melos monorepo with multi-app + shared core_* packages, structured from day one

Status

Accepted (pending FieldForce Phase 1 implementation)

Tags

fieldforce, digital-worker, mobile, flutter, monorepo, melos, modules, ddd

Decision

The Flutter codebase is a melos-managed monorepo with an apps/ + packages/ split, structured for multiple apps from day one even though FieldForce ships first.
gremlin_mobile/
  apps/        fieldforce/  digital_worker/      # each a full, independently deployable app
  packages/    core_models/ core_network/ core_auth/ core_database/
               core_ui/ core_i18n/ core_notifications/ core_location/
  • Each apps/* is a full Flutter app with its own bundle ID, flavors, and release cadence.
  • Shared infrastructure lives in core_* packages, depended on via path.
  • Infrastructure is shared from day one; features earn sharing only when a second app needs them (start in the app, graduate to packages/feature_* on the second consumer — never pre-share).
  • Apps deploy separately (separate bundle IDs, versions, signing, store listings, CI triggers). The monorepo is a development-time convenience, not a deployment coupling.

Why

Two apps are already planned (FieldForce, Digital Worker) and they share substantial infrastructure: offline-sync wiring, auth/session, brand UI, domain models, notifications. Building these once in shared packages — rather than copy-pasting across two single-project apps — is the core leverage of a monorepo, and it mirrors the backend’s shared-kernel / bounded-context approach (ADR-0004, ADR-0009). Structuring for the monorepo shape now, with one app, costs almost nothing and makes the second app a git mv + a pubspec path entry rather than an extract-everything migration later. The project explicitly wants to “get it right at the beginning” and avoid retrofits. Crucially for a vibe-coded workflow, separate packages enforce boundaries the compiler can check — a shared package physically cannot import from an app, so the LLM cannot generate a cross-boundary import that violates the architecture. The dependency graph itself enforces the design, not just CLAUDE.md prose. Rejected alternatives:
  • One Flutter project per app (separate repos / no shared packages). Simplest to start, but guarantees copy-paste drift of auth/sync/brand code across apps and a painful later consolidation. Rejected — the shared offline-sync engine and brand UI alone justify sharing.
  • Single-project app with a lib/core/ folder (no packages). Boundaries become convention, not compiler-enforced; nothing stops features/ leaking into core/. Rejected — package boundaries are the enforcement mechanism that matters most under vibe coding.
  • Full monorepo scaffolding with speculative empty packages. Over-building. Rejected — a core_* package is born when its first real code lands, not before.

Consequences

  • Path dependencies mean all apps always use HEAD of shared packages — a breaking change in core_network breaks every app at once. Atomic refactors are a benefit; the cost is that shared packages carry the strictest test coverage (they are load-bearing for N apps).
  • CI must use path-based triggers (affected-graph) so each app builds only when its code or a dependency changes (see ADR-0045).
  • iOS signing scales per-app — each app needs its own provisioning, certs, push keys; the monorepo does not reduce this.
  • A package dependency DAG must be defined and enforced (see ADR-0039).

Rules for agents

  • Each apps/* is independently deployable with its own bundle ID; the monorepo never couples release schedules.
  • Infrastructure goes in core_* packages from day one; feature code starts in the app and graduates to packages/feature_* ONLY when a second app needs it. Never pre-share.
  • Do NOT create empty packages speculatively — a package is born with its first real code.
  • A package MUST NOT import from an app; respect the dependency DAG (ADR-0039).
  • Use melos run generate for codegen across packages; CI uses path-based triggers.