RORK LABJP
APPLE-AI — Apple opens Foundation Models free to developers under 2M first-time downloads, slashing the cost of adding AI to indie appsSWIFT-API — Foundation Models server-side integration lets you call Claude and Gemini through the same Swift API, now with image inputKOTLIN-MIGRATION — Android Studio's migration agent converts React Native apps into native Kotlin automatically — a future path for Rork-built appsRORK-MAX — Rork Max generates native Swift code ($200/mo), covering iPhone, iPad, Watch, TV, Vision Pro, and iMessageSIMULATOR — A browser-based streaming iOS simulator lets you test on a real Apple environment without Xcode or Mac hardwareSWIFTUI — SwiftUI evolves at WWDC 2026 with reorderable containers, swipe actions for any container, and layouts up to 2x fasterAPPLE-AI — Apple opens Foundation Models free to developers under 2M first-time downloads, slashing the cost of adding AI to indie appsSWIFT-API — Foundation Models server-side integration lets you call Claude and Gemini through the same Swift API, now with image inputKOTLIN-MIGRATION — Android Studio's migration agent converts React Native apps into native Kotlin automatically — a future path for Rork-built appsRORK-MAX — Rork Max generates native Swift code ($200/mo), covering iPhone, iPad, Watch, TV, Vision Pro, and iMessageSIMULATOR — A browser-based streaming iOS simulator lets you test on a real Apple environment without Xcode or Mac hardwareSWIFTUI — SwiftUI evolves at WWDC 2026 with reorderable containers, swipe actions for any container, and layouts up to 2x faster
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.

Rork382AsyncStorage9Data MigrationEAS Update4Long-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-05-29
Rork × EAS Update Runtime Version Strategy — Upgrading Expo SDK Across 6 Apps Without Breaking Existing Users
A complete record of how I migrated 6 Rork-generated apps from Expo SDK 50 to 51 in three weeks without a single user-visible incident — runtimeVersion policies, full eas.json, a safety-gated publish script, and a 30-minute incident recovery playbook.
Dev Tools2026-05-28
Three Weeks of Moving Six Wallpaper Apps from AsyncStorage to MMKV in Rork
Notes from three weeks of gradually moving six wallpaper apps from AsyncStorage to react-native-mmkv. Personal write-up from an indie developer who has been shipping iOS and Android apps since 2014.
Dev Tools2026-05-19
Fixing 'Row too big to fit into CursorWindow' on Android When AsyncStorage Holds Too Much in Rork
When a React Native app generated with Rork stores large JSON or image metadata in AsyncStorage, Android can throw a Row too big to fit into CursorWindow exception. Here are the practical fixes — MMKV migration, chunked keys, payload trimming, and compression — explained from real wallpaper-app experience.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →