← All articles
8 min readData,Migrations,Local First,Schema

Versioning Local Data: Stamps, Snapshots and Derived State

By Mykhailo Boichuk · Chief Technology Officer

In short

Flowo, the planning app from BBMM Technologies, has no single schema version for an install. Versions are attached to the thing that changes: the name of a migration marker, a schema_version field inside each small state document, and a version on messages sent to the planner's worker. Before its largest migration the app wrote a snapshot of seven data sets, checked it by SHA-256 and byte length, and gave it a 30 day expiry.

Where the version numbers live

Flowo stores no global schema version for an install. Each piece of data that can change shape carries its own version, close to where it is read.

  • Migration markers. Each one-shot startup migration writes a key such as migration:event_start_time:v1. The version is part of the key name, so a second version of the same migration would be a new key and would run once more.
  • State documents. Small maps written to local storage carry a schema_version field, and the planner migration state lives in the app_meta box. The planner migration state, the snapshot manifest, the snapshot payload and the notification ledger are all at version 1. The planner's stored report and its snoozes are at version 2.
  • Storage names. Some keys and box names carry the version as a suffix, for example a V1 on the migration state key and a v3 on the chat boxes.
  • Worker messages. A planning request sent to the worker isolate carries a schema version, currently 12, and each increase is recorded in a comment that names the field it added.

A companion article covers the marker mechanism and the stub adapters for retired Hive type ids. This article covers the versioned documents, the snapshot and the question of what a migration may delete.

How a reader treats an unknown version

We use two reading policies, and the choice depends on whether the data can be rebuilt.

A control document is rejected when anything about it is wrong. The reader of the planner migration state throws on a version other than 1, on a fractional version, on a stage name it does not know, on a timestamp that lacks a UTC offset and on a negative attempt count. One test feeds it nine malformed documents and expects every one to be refused.

A rebuildable document is dropped when its version does not match. The planner's stored report and snoozes are read as absent when the version differs or when the document is larger than 256 KB. The planner writes a fresh report on its next pass.

Optional fields keep older records readable without a version bump. The migration state gained two retry fields after it first shipped, and a record written before them reads back with zero attempts spent. The planner report gained one optional key the same way. We left its version alone on purpose, because the snoozes share that version and a bump would have dropped every person's snoozes once.

What the planner snapshot contains

Before the migration onto the current planning engine rewrote anything, Flowo captured a snapshot with seven parts.

  1. 1.Every task.
  2. 2.Every block on the timeline.
  3. 3.The settings box.
  4. 4.The categories box.
  5. 5.Stored planning results.
  6. 6.The planner's learned weight matrix, as one document.
  7. 7.Three planner metadata keys from app_meta.

The snapshot is encoded with Hive's own binary writer, so each stored object is written by the same type adapter that wrote it to its box. The bytes are saved as a single value in app_meta under a key that includes the backup id.

A separate manifest records the backup id, the creation time, the expiry time, the SHA-256 of the bytes and their length.

How the snapshot is verified

The snapshot is read back from storage before the migration is allowed to continue. The stored bytes must have the same length and the same SHA-256 as the bytes just written, and they must decode into a valid snapshot. Only then is the manifest written.

The same verification runs again before a restore. It also checks that the manifest id matches the id being restored and that the expiry has not passed. The default retention is 30 days, and an expired snapshot no longer verifies.

A snapshot that fails verification is never applied. The app logs the fact, leaves the data as it stands and finishes the migration state machine anyway, so a damaged backup cannot leave an install stuck.

Restoring replaces the same seven parts. Tasks and timeline blocks are written back through the atomic paths of their sources, and metadata keys absent from the snapshot are deleted.

What a migration is allowed to discard

Each Flowo migration that removes or rewrites data states in its own documentation why nothing is lost. The reasoning is always that the removed value can be derived from something that stays.

One migration cleared a done flag that an older build had allowed on habits. A habit's record is its confirmed blocks, and the streak is recomputed from those, so the flag held no information. The same migration leaves the archive flag untouched. Bringing an archived habit back would restart its scheduling without being asked, and the app already has an explicit unarchive action.

Another removed the repeat rule from flexible habits. The period of a flexible habit is read from its quota fields, so the rule was never used for it.

A third deleted system blocks written by an old scheduler. It removes only blocks it can positively identify by their parent task and their type. The current planner places those needs itself.

A related rule applies to the planner migration: nothing about a schedule may survive a launch boundary. The setup step builds its input from live settings at the moment of the attempt. A draft saved on one launch and applied on a later one would overwrite whatever the person built in between.

State held by the operating system

Some state that needs migrating is outside our storage. Pending reminders are held by the operating system under numeric ids, and an older Flowo build stored those ids on schedule blocks.

The migration away from those ids collects them, asks the system which notifications are pending and cancels the ones that match. It writes its completion flag only when no cancellation failed. A partial failure therefore repeats at the next start, and cancelling an id that is already gone is skipped.

A version stamp tells a reader what shape to expect. It does not make a migration safe. Safety comes from the order of writes, and from knowing which values can be rebuilt.

How version handling is tested

The planner migration is tested by simulating a process death after each stage has been saved. The test throws at one of five stages, builds a new coordinator over the same storage and calls the startup recovery twice. The install must end in the active stage with no restore performed.

The snapshot store has its own tests for a corrupted blob, for a restore that fails halfway and is repeated, and for expiry after 30 days. Manifest parsing is tested against malformed fields in the same way as the migration state.

Key takeaways

  • Flowo has no install-wide schema version, and each marker, state document and worker message carries its own.
  • Control documents are rejected on any malformed field, while rebuildable documents with the wrong version are dropped and written again.
  • Adding optional fields with defaults let two documents change shape without a version bump.
  • The planner snapshot holds seven data sets and is verified by byte length, SHA-256 and a decode before the migration proceeds.
  • A snapshot expires 30 days after creation, and an expired or damaged snapshot is never restored.
  • A migration may delete a value only when it can be derived from data that stays on the device.

Frequently asked questions

Where should a mobile app store its data schema version?
Flowo, from BBMM Technologies, stores no single version for the whole install. Each migration marker has the version in its key name, and each small state document carries a schema_version field that its reader checks. A change to one document then needs no change to the others.
What should an app do when it reads a document with an unknown version?
It depends on whether the document can be rebuilt. Flowo rejects control documents such as its migration state with an error. It drops rebuildable documents such as the planner's stored report and writes a new one on the next planning pass.
How do you verify a backup made on the device before a migration?
Flowo reads the snapshot back from storage, compares its length and SHA-256 with the bytes it just wrote, and decodes it. The manifest is written only after those checks pass. The same checks run again before any restore.
Can a field be added to stored data without bumping the schema version?
Yes, when the reader gives a missing field a safe default. Flowo's migration state gained two retry fields this way, and an older record reads back as having spent no attempts. A version bump is kept for changes an older shape cannot express.
How do you test a migration that can be interrupted?
Flowo's tests throw a simulated process death after each stage of the planner migration is saved. A new coordinator is then built over the same storage and startup recovery runs twice. The test passes only if the install reaches its final stage.

References

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.