Migration-Safe Schemas for an On-Device Database
By Mykhailo Boichuk · Chief Technology Officer
In short
Flowo keeps each person's data in encrypted Hive boxes on their own phone, so BBMM Technologies cannot run a migration centrally or roll one back. We keep schema changes safe with four habits: retired type ids stay readable through a stub adapter, each startup migration leaves a receipt, every destructive rewrite is preceded by a backup, and an interrupted migration is resumed at the next launch.
Why an on-device schema is harder to change
Flowo's database lives on the phone, in Hive boxes that only that device can read. There is no server copy to migrate once, and no moment when all installs share the same shape.
A person may open a new build over data written by any earlier build, including one from many versions back. The migration code therefore runs on a device we cannot observe, at launch, with the process free to die at any line. Everything below follows from that.
How retired type ids stay readable
Hive identifies each stored class by a numeric type id, and it throws an unknown typeId error when it meets an id with no registered adapter. Deleting a model class would therefore crash the next launch for anyone who still has that class on disk.
We register a LegacyStubAdapter for every retired id. Its read method consumes the field count and then each field index and value, so the reader's cursor lands exactly where the next value begins. It returns null. Callers treat that null as missing data.
The stub list covers the gaps left by removed models inside our active range of 0 to 60, and it reserves 61 to 99 in full. Adding a model with a new type id means removing that id from the stub set in the same change.
All adapters, real and stub, are registered in one function that runs before Hive is initialised and before the first box opens.
How fields are added
A new field gets a new field index and a declared default. Hive adapters are generated from annotations, and a record written before the field existed simply has no value at that index. The default declared on the annotation fills it in at read time.
We commit the generated adapter files in the same change as the model. A model and its adapter can then never disagree in a build.
How startup migrations are marked
Data migrations run once per install, inside a single function called runStartupMigrations. It runs seven steps in a fixed order after the boxes are open and before the task manager is constructed.
Each one-shot step begins by looking for its own marker key in a metadata box, for example migration:event_start_time:v1. If the marker is present the step returns immediately. When the step finishes it writes the marker as a small map holding a version number and a count of the records it changed.
Every step is written so that a partial run can repeat. One step moved tasks out of two placeholder category names that earlier builds had invented. A task already moved no longer matches the filter, so a second pass after a crash touches only what remains.
One step has no marker on purpose. It deletes hidden categories whose habit no longer exists. It is cheap and has nothing to do when the data is clean, so it runs on every launch and also repairs anything a later bug leaves behind.
How a storage format change is made safe
Early Flowo builds stored boxes unencrypted, and later builds had to move that data into AES-256 encrypted boxes without losing it. Handing a cipher to an existing plaintext file is a data-loss bug in Hive. The file fails its checksum and Hive treats it as damaged. Its crash recovery then leaves an empty box.
Our storage service inspects the file before Hive is allowed to open it. It distinguishes a plaintext file from one encrypted with the current key, and both from a file the current key cannot read. Only the plaintext case leads to a migration.
- 1.Write an encrypted backup copy of the plaintext file.
- 2.Open the file without a cipher and copy every entry into memory.
- 3.Close the box and delete the file.
- 4.Open a new encrypted box under the same name and write the entries back.
- 5.Record a receipt for that box and delete the backup.
A transient failure such as a file lock is retried once. A migration cut short by process death is restored from its backup and retried at most twice, and after that it is recorded as an incident together with the backup's location. Recovery backups are kept for 30 days and capped at 200 MB in total.
How the planner migration recovers
The largest migration we have shipped moved every install onto a new planning engine, and it is driven by a small state machine. The stages are legacy, backingUp, applyingSetup, migratingSeries, enabling and active, with restoring used after a failure.
The current stage is saved to the metadata box after each transition, and a recovery call at every launch resumes whatever stage was in progress. The saved state has its own schema version and is validated field by field when read.
The two working steps follow different retry rules. Applying the new settings rewrites a person's configuration, so it gets one attempt in the life of the install. Rebuilding repeating items over a fixed horizon is idempotent, so it may be tried on up to three launches. The attempt counter is incremented before each try. A crash therefore costs an attempt, and a crash loop ends.
A snapshot is created and verified before anything is rewritten. On failure the snapshot is restored if it still verifies, and the install settles in the active stage. The snapshot id is then cleared, so a later retry cannot put back a schedule the person has since replaced.
Key takeaways
- Flowo registers a LegacyStubAdapter for every retired Hive type id, so data written by an old build is skipped as null and does not crash the launch.
- Each one-shot startup migration writes a versioned receipt to a metadata box and exits early when it finds that receipt.
- Migration steps are written to be repeatable, because the process can be killed between any two writes.
- The plaintext to encrypted storage migration writes a backup before it opens the box and keeps recovery backups for 30 days.
- The planner migration counts an attempt before it runs and gives up on its retryable step after three launches.
- A restored snapshot is marked as spent, so it can never overwrite newer data on a later retry.
Frequently asked questions
- What happens in Hive when a model class is deleted?
- Hive throws an unknown typeId error when a box contains a value whose type id has no registered adapter. Flowo avoids this by registering a stub adapter for each retired id. The stub reads past the stored bytes and returns null.
- How do you run a Hive migration only once?
- In Flowo each migration checks a marker key in a metadata box before doing any work, and writes that key when it finishes. The marker holds a version number and a count of changed records. A step interrupted before the marker is written runs again at the next launch, so each step is written to be repeatable.
- How can a plaintext Hive box be encrypted without losing data?
- Opening a plaintext file with a cipher makes Hive treat it as damaged and replace it with an empty box. Flowo first checks what the file contains, writes a backup, reads the entries without a cipher, deletes the file, and writes the entries into a new encrypted box. A receipt records that the box is done.
- What does Flowo do if a migration is interrupted?
- The planner migration saves its stage after every transition, and a recovery call at each launch resumes from that stage. The storage migration restores from its backup and retries, at most twice. In both cases the retry count is bounded so a failing device cannot loop forever.
- Can a mobile app roll back a database migration?
- There is no central rollback for data that lives on a phone. Flowo takes a verified snapshot before the planner migration rewrites anything and restores it on failure. The snapshot is then discarded so it cannot replace newer data later.
References
- Flowo
- BBMM Technologies
- Flutter documentation
- Hive package documentation: type adapters and encrypted boxes
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.