◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-12Advanced

Schema Versioning for Local Data in Rork Apps — Shipping Updates Without Wiping a Single Favorite

How I stopped losing users' locally stored data when shipping updates to Rork apps. A complete TypeScript migration runner with envelope versioning, backup keys, fixture tests, and the rule that keeps EAS Update schema-neutral.

Rork611AsyncStorage11Data MigrationEAS Update8Long-term Maintenance

✦ Premium Article

Last winter, the morning after I shipped an update to one of my wallpaper apps, two one-star reviews were waiting for me. Both said essentially the same thing: "All my favorites are gone."

The cause was a change in storage format. I had been saving favorites in AsyncStorage as a plain array of wallpaper IDs (string[]), and the update switched to an array of objects carrying a timestamp. The new code could only read the new shape, so it overwrote the old data with an empty default. Favorites that users had collected over months disappeared because of roughly ten lines I changed.

A server database would never fail this way. You write a migration, run it before deploy, and roll back if it breaks. That discipline is taken for granted on the backend. Yet a surprising number of apps — mine included, at the time — operate local on-device data with no such discipline at all.

After that morning, I moved every app I build with Rork onto a shared migration layer. What follows is the full implementation, plus the operational rules that settled into place while running several apps in parallel over the long term.

Why Local Data Breaks More Easily Than a Server Database

A server migration runs once, against one database. Local data is different. There are as many databases as there are devices, and each one migrates at its own moment, from its own starting version. Devices jumping three versions at once are not rare at all.

And you cannot reach any of them. There is no production server to SSH into. The only repair tool you have is "ship fix-up code in the next update" — and by the time it arrives, the user may have already deleted your app. As an indie developer, you are also the support desk that receives the complaint.

Rork apps, being Expo-based, add one more wrinkle: OTA delivery through EAS Update. Replacing only the JavaScript without store review is a real advantage, but it also raises the frequency of the "new code, old data" state. Because OTA rollbacks are instant too, you can even get the reverse mismatch: "old code, new data."

Building with an AI tool amplifies all of this. Ask Rork to "add a saved-at timestamp to favorites" and it will happily rewrite the types and the UI in one pass. What it will not consider — unless you tell it — is the old-format data already sitting on users' devices. The faster you can change code, the more often code and data drift apart. That is exactly why the discipline around local data needs to be in place first.

The Core of the Design Is a Single Integer — Envelopes and Schema Versions

The rule I apply across all my apps is simple: every JSON value goes into an "envelope" before it is stored.

{ "v": 3, "data": [{ "id": "w_201", "addedAt": 1749690000000 }] }

The shape of the payload (data) is free to change as often as it needs to. In exchange, every shape change bumps the envelope version v by one and comes with exactly one function that converts v2 data into v3 data. At read time, you apply the conversion functions in order, from the stored version up to the current one. That is the whole design.

One detail matters: do not encode the version into the key name. Once you start creating keys like favorites_v2, you end up managing migrations and stale-key cleanup as two separate problems, and soon nobody can say which key is authoritative. Keep one key, and let only the integer inside the envelope move forward. In my experience this is the shape least likely to produce accidents.

The earlier question — whether to use AsyncStorage, MMKV, or SQLite in the first place — is covered in my comparison of the three local storage options, so here I will stay focused on protecting the data after you have chosen.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A complete TypeScript migration runner that wraps AsyncStorage values in a versioned envelope
✦The operational boundary that keeps EAS Update (OTA) schema-neutral, and the rollback failure scenario behind it
✦A measurement recipe for deciding how long to keep backups and when old migration functions can be retired
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-10-27
Your EAS Update Opens in Expo Go on iPhone but Stops on Android with "Failed to download remote update". The 403 You Get Even When Signed In
An EAS Update that opens in Expo Go on iOS but stops on Android (SDK 57) with Failed to download remote update. The cause is a sign-in token saved in one place and read from another, and the fix sits on the SDK 58 side. The order to check things in, and what to run yourself.
⬡ Dev Tools2026-10-30
It runs in development but the iOS Release build dies on launch — checking whether a missing scheme is the cause
In an expo-router app, an app.json with no scheme can crash only the iOS Release build at launch. Why Debug and Android survive, the two ways to fix it, how to think about leaving the scheme out on purpose, and a pre-submission check.
⬡ Dev Tools2026-10-27
Claude Sonnet 4.5 Leaves the API on November 30. Find the Model IDs Hard-Coded in Your App First
Claude Sonnet 4.5 is scheduled to retire from the API on November 30, 2026. A small script finds the model IDs written into your app's code and config, so you can replace them and check again.