Skip to content

ADR-0007: Drift as the local-first store on Flutter

Status: Proposed
Date: 2026-05-10
Owner: @satya
Related: ADR-0001 (Flutter), ADR-0002 (Supabase), spec 0001 (Trip Planner)

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).

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.

OptionWhy not
IsarFast and ergonomic, but the v3→v4 rewrite stalled and core maintenance is uncertain.
Hive / sqflite + hand-rolled DAOEither no relational support (Hive) or no codegen / type-safe queries (sqflite). Outbox semantics + drag-reorder transactions get painful fast.
ObjectBoxGood perf, commercial license complexity, smaller community than Drift.
PowerSync / ElectricSQLBring 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 filesWon’t meet “indefinitely available offline” or transactional reorder.

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/push semantics stay obvious.

Negative

  • Build-time codegen step (build_runner) adds friction; mitigated by --delete-conflicting-outputs aliases in make targets.
  • SQLite native libs slightly inflate iOS/Android bundle sizes (~1 MB).
  • Schema changes require coordinated mobile + backend migration order.
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.dart
  • schemaVersion: 1 ships with trips + outbox tables only.
  • M2..M10 each bump schemaVersion and add a MigrationStrategy step.
  • All cubits depend on a DAO interface, not Drift directly, so tests use an in-memory NativeDatabase.
  • 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.