← All articles
7 min readLocal First,Architecture,Offline

Local-First on One Device: What Flowo's Architecture Costs

By Mykhailo Boichuk · Chief Technology Officer

In short

Flowo, the planning app from BBMM Technologies, keeps the only copy of a person's plan in encrypted storage on their phone, so the app does the housekeeping a server would normally do. That work is specific: a fixed startup order, a lease file that keeps a background isolate away from open storage, planning on an in-memory copy with a rollback when a write fails, and one inventory of 24 stores that erasure follows.

What local-first means in Flowo

In Flowo, the phone holds the only copy of a person's tasks and schedule, in Hive boxes encrypted on the device.

No server holds a second copy. Jobs that a hosted product hands to its database and its operators become jobs for the app. The app has to open storage in a safe order, keep two writers apart, apply a change completely or leave the previous plan in place, and delete everything when the person asks.

We describe the absence of multi-device sync in a separate article. This one covers what the design costs inside a single install.

The startup order

Flowo starts in four steps that always run in the same order: secure storage, environment, security checks, storage.

The first step resolves the storage key from the iOS Keychain or the Android Keystore. No box can be opened without that key, so it comes before everything else and a failure here stops the launch.

The storage step registers every Hive type adapter, initialises Hive and claims a lease file. Only then does the first box open. Boxes that hold a person's data are opened one at a time through a single encrypted-open call in our storage service, and that service is the only place in the app that calls Hive's own open function.

If a step throws, the app shows a failure screen with the name of the step, a retry button and a way to copy the technical detail. If startup has not finished after 8 seconds, the splash screen changes its text to say it is taking longer.

Opening boxes is the heaviest file work at launch. We pause for 32 milliseconds between the heavy opens so the splash animation keeps drawing. An earlier 16 millisecond pause was too short, because one Hive file operation sometimes takes 8 to 12 milliseconds.

One writer at a time

Two parts of Flowo can touch the same files: the app on screen, and a background job that tops up reminders about once a day.

On Android that job runs in a second isolate of the same process. Hive keeps separate box instances for each isolate, and its file lock is advisory and held per process, so both isolates acquire it. A background isolate that opens the tasks box while the app is halfway through appending a record sees an incomplete final frame. Hive's crash recovery then truncates the file at that point.

We prevent this with a lease, a plain file that holds a UTC timestamp. The app writes it before opening its first box and rewrites it every 2 minutes. The background job reads it before doing anything else and exits when the stamp is younger than 10 minutes.

The lease expires by age because a process killed by the operating system never gets to release it. A lease that cannot be parsed counts as held, and so does one stamped in the future. Skipping one top-up costs a few late reminders, and a wrong guess could cost a person's tasks.

The background isolate also receives a narrower storage service. It may open a box that is already encrypted with the current key, and it refuses any box that would need migration or recovery.

Planning on a copy

The planner does not edit the stored schedule while it works. It copies the timeline into an in-memory source, plans there, and returns a report with a status for every task. Nothing has been written at that point.

The planner gateway runs the computation in a separate isolate. The request is encoded into primitive values and carries a schema version, currently 12, and a worker that receives any other version rejects the message. If the planning inputs change while the worker is running, the result is thrown away as stale and the app plans again, up to 3 times.

Hive has no transaction across boxes, so the commit is built by hand. Before writing, the timeline source records the previous value of every block it is about to touch. It writes the additions and then the deletions. If a write fails, it removes what was newly added and puts the old values back.

Storage sits behind small interfaces for tasks, the timeline, days and date overrides. Each interface has a Hive implementation and a memory implementation. The planner uses the memory one for its working copy, and our tests use it to run the engine with no disk.

One inventory of stores

Every Hive box that Flowo owns is listed in one registry, which has 24 entries today. Each entry declares the same fields.

  • A data classification.
  • Whether encryption is required.
  • A retention rule.
  • Whether the box is included in a data export.
  • How the box is erased.
  • A backup policy.
  • The module that owns it.

The delete flow takes its list of boxes from this registry, and a test compares the two sets. A box that is registered and missing from erasure fails that test.

Erasure deletes every box file from disk and then deletes the storage key from the Keychain or Keystore. If any box fails to delete, the key is kept and the failure is reported. Tests cover both branches.

A hosted product can answer a deletion request by running a query. In a local-first app the device has to do it alone, including the key, so we treat the registry as part of the data model.

What the design suits and what it costs

This design suits data that belongs to one person and outputs that can be rebuilt from inputs. A Flowo schedule is computed from tasks and settings, and reminders are computed from the schedule. A damaged timeline can be planned again.

The first cost is that the plan exists on one device. The second is that every guarantee described here is code we wrote and have to test, and it runs on phones we cannot inspect.

A storage bug that we ship reaches data of which we hold no copy. That is the reason each mechanism above fails toward keeping what is already on disk. An unreadable lease is treated as held, and a failed write is rolled back.

Key takeaways

  • Flowo starts in four fixed steps, and the storage key is resolved before any Hive box is opened.
  • A lease file rewritten every 2 minutes and valid for 10 keeps the daily background job from opening boxes while the app is running.
  • The planner works on an in-memory copy of the timeline and writes nothing until its report is ready.
  • Timeline commits record previous values first and restore them if a write fails, because Hive has no transaction across boxes.
  • One registry lists all 24 Hive stores, and a test fails when the erasure list differs from it.
  • During erasure the storage key is deleted only after every box file is gone.

Frequently asked questions

What does local-first mean for a mobile app?
It means the device holds the primary copy of the data and the app works without a network. In Flowo, the planning app from BBMM Technologies, tasks and the schedule live in encrypted Hive boxes on the phone, the planner runs on the device, and reminders are local notifications.
Why can two isolates damage a Hive box?
Hive keeps separate box instances for each isolate, and its file lock is advisory and held per process. A second isolate that opens a box during a write can read an incomplete final frame, and Hive's crash recovery truncates the file there. Flowo avoids this with a timestamped lease file that the background job checks before it opens anything.
How does Flowo apply a new schedule without a database transaction?
The planner computes the schedule on an in-memory copy and returns a report first. On commit, the timeline source saves the previous value of each block it will touch, writes additions and then deletions, and restores the saved values if a write fails.
What happens when Flowo cannot open its storage at launch?
Startup runs as four ordered steps, and a step that throws stops the sequence. The app shows a failure screen that names the step and offers a retry. It does not continue with partial storage.
Does a local-first app still need a backend?
Flowo plans and schedules reminders on the device with no connection. Its backend is used for cloud AI requests that the person starts, such as breaking a task into steps. The backend is not where the plan is stored.

References

  • Flowo
  • BBMM Technologies
  • Flutter documentation
  • Kleppmann, Wiggins, van Hardenberg, McGranaghan: Local-first software (Ink & Switch, 2019)
  • Dart language documentation: Isolates
  • Hive package documentation: boxes, type adapters and encryption

About the author

Mykhailo Boichuk

Chief Technology Officer

Mykhailo builds the systems behind the apps. He works on macOS and iOS performance, and on the sync problems that come with software that has to run offline.