ADR-0007: Drift as the local-first store on Flutter
Status: ProposedDate: 2026-05-10Owner: @satyaRelated: ADR-0001 (Flutter), ADR-0002 (Supabase), spec 0001 (Trip Planner)Context
Section titled “Context”Spec 0001-trip-planner requires:
- 100% offline reads on an opened trip (F1.10.a, AC-8).
- Queued writes that replay when the device reconnects (F1.10.b, AC-9).
- Last-write-wins per field across two devices (F1.10.c, AC-10).
- Sync state visible in the UI (F1.10.d).
State management is already settled: flutter_bloc is in pubspec.yaml
and used by the auth + home features. This ADR is only about where
trip data lives on-device and how the outbox is persisted.
The store has to support:
- Relational data with foreign keys (trips → days → activities → links / attachments).
- Reactive queries (UI rebuild on row change).
- Transactions for batched ops (drag-reorder of N activities).
- A durable outbox table for sync replay.
- Migrations alongside Supabase migrations (schema evolves together).
- Encrypted-at-rest option for v1+ (not gating v0).
Decision
Section titled “Decision”Use Drift (SQLite) as the on-device store for Treeper mobile. Outbox is a Drift table. Bloc cubits read via Drift streams; writes hit Drift first (optimistic), then enqueue to the outbox.
A thin TripRepository mediates between cubits and Drift; the sync engine
runs in a background isolate spawned at app start, listens to outbox
inserts, and calls the NestJS sync API.
Alternatives considered
Section titled “Alternatives considered”| Option | Why not |
|---|---|
| Isar | Fast and ergonomic, but the v3→v4 rewrite stalled and core maintenance is uncertain. |
| Hive / sqflite + hand-rolled DAO | Either no relational support (Hive) or no codegen / type-safe queries (sqflite). Outbox semantics + drag-reorder transactions get painful fast. |
| ObjectBox | Good perf, commercial license complexity, smaller community than Drift. |
| PowerSync / ElectricSQL | Bring sync-as-a-service. Tempting but locks us in early; spec’s LWW-per-field is simple enough to own. Re-evaluate at v1. |
| Pure in-memory + JSON files | Won’t meet “indefinitely available offline” or transactional reorder. |
Consequences
Section titled “Consequences”Positive
- Type-safe queries via codegen; bloc cubits get reactive streams for free.
- One file (
treeper.db) on disk; easy to back up, inspect, and reset. - Migrations live in code under
apps/mobile/lib/core/db/, version-numbered. - Outbox is just a table —
pull/pushsemantics stay obvious.
Negative
- Build-time codegen step (
build_runner) adds friction; mitigated by--delete-conflicting-outputsaliases inmaketargets. - SQLite native libs slightly inflate iOS/Android bundle sizes (~1 MB).
- Schema changes require coordinated mobile + backend migration order.
Implementation notes (for M1)
Section titled “Implementation notes (for M1)”apps/mobile/lib/core/db/ treeper_database.dart // @DriftDatabase, schemaVersion: 1 tables/ trips.dart trip_days.dart // empty in M1, populated in M2 daos/ trips_dao.dart outbox/ outbox_table.dart outbox_dao.dartschemaVersion: 1ships withtrips+outboxtables only.- M2..M10 each bump
schemaVersionand add aMigrationStrategystep. - All cubits depend on a DAO interface, not Drift directly, so tests use an in-memory NativeDatabase.
Follow-ups
Section titled “Follow-ups”- ADR-0009 will revisit conflict policy if LWW-per-field proves too lossy in beta (e.g. simultaneous notes edits on two devices).
- v1: enable SQLCipher for encryption-at-rest if a privacy review demands it.