●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round●TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13●OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude Code●SIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App Store●MAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessage●NATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core ML●SEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
A Phased Release Strategy for Rork Apps — How I Combine iOS Phased Release, Android Staged Rollout, and OTA Hotfixes for a Production That Doesn't Break
How to protect a Rork app's production with three layers of safety: iOS Phased Release, Android Staged Rollout, and EAS Update OTA hotfixes — with concrete decision thresholds and monitoring code you can copy today.
For an indie developer running an app long-term, the scariest few hours are always right after pushing a new version. I've been operating apps with a combined 50 million downloads for over a decade, and more than once I've shipped a quiet evening update only to wake up to "the app won't open" and "my data is gone" filling the review feed. Apps built with Rork are no exception. The AI generates capable code, but no implementation ever runs perfectly across every device, locale, and network condition.
This article is about removing that fear with structure rather than willpower. Specifically, I'll show you how to combine iOS Phased Release, Android Staged Rollout, and EAS Update OTA hotfixes as three layers of safety — limiting the blast radius from "every user" to "1–10% of users," and shrinking recovery time from a day to a few hours.
I'll also cover the practical pitfalls the official docs don't mention — what happens when you submit a fix mid-Phased-Release, why native crashes can't be patched over the air, and how to write an expedited review request that actually gets approved — with the exact judgment calls I make in production.
Why "production that doesn't break" matters more for indie devs
A larger company has a support team that absorbs the impact of a bad release. As a solo developer, the only person who can answer "the app crashes on launch" emails is you, and while you sleep the App Store review feed keeps filling up. I've watched the metrics: ten one-star reviews on a release day depresses new downloads for months afterwards.
So the goal of production for an indie developer isn't "ship zero bugs." It's "when a bug ships, contain the damage." That's a meaningful shift in mindset. Aiming for perfect releases is less productive than building a system where shipping incomplete work is still safe.
The carpenter grandfathers I had — both of them built shrines for a living — taught me that careful work isn't about getting it right the first time. It's about building things that can be repaired when they break. Phased release strategy in production is exactly that kind of structure.
The three-layer safety net at a glance
Before diving into implementation, here's the shape of the three-layer net we'll build.
Layer 1 — store-level gradual rollout. iOS Phased Release (1% on day 1 ramping to 100% over 7 days) and Android Staged Rollout (any percentage you choose). Always on, by default. This physically prevents day-one releases from reaching every user at once.
Layer 2 — automated monitoring and decision support. Sentry or Firebase Crashlytics watching crash-free rates per release; a GitHub Actions job that pages you on Slack/Discord when thresholds break, with all the context attached.
Layer 3 — recovery paths chosen by category. JavaScript-layer bugs go out via EAS Update (OTA) almost instantly. Native-layer crashes or policy issues go through Apple's Expedited Review. A documented rollback procedure for the worst case.
Setting all three up takes about half a day the first time. After that, every release becomes "push the button, watch the numbers, pause if needed."
✦
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
✦If a release once put a bug in front of every user at the same time, you'll walk away with a setup that limits the blast radius to 1–10% of users from now on.
✦You'll learn the exact numeric thresholds I use to decide whether to pause a phased release, plus a Sentry + GitHub Actions monitor you can drop in this weekend.
✦You'll be able to tell native crashes from JS bugs at a glance and pick the right recovery path — OTA or expedited review — cutting recovery time from a full day to a few hours.
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.
Phased Release is an App Store Connect feature. When enabled, your new version rolls out to existing users on this fixed schedule:
Day 1: 1%
Day 2: 2%
Day 3: 5%
Day 4: 10%
Day 5: 20%
Day 6: 50%
Day 7: 100%
Notice the day-one number: 1%. If your app has 100,000 DAU, that's at most 1,000 users impacted on day one. That alone removes most "review storm" risk.
Pitfall the docs don't spell out, #1: it only applies to updates
Phased Release applies only to automatic updates of existing users. New downloads — from search, ads, or manual installs — get the latest version immediately. So if you're running search ads or have a steady inflow of new users, day-one is not actually as quiet as it looks.
I learned this the hard way when I assumed "Phased Release means day one is safe" and missed a crash that was hitting new users for half a day before I noticed. If your app sees significant new-user inflow, tighten the Layer 2 monitoring threshold accordingly.
Pitfall the docs don't spell out, #2: submitting a fix during Phased Release
If you spot a serious bug mid-Phased-Release and submit a fix, here's what happens when the fix passes review: the in-progress Phased Release is automatically cancelled, and the new version's Phased Release starts over from 1%.
This is correct behavior, but it confuses people in the moment. Even after your fix is approved, only 1% of users get it on day one — meanwhile, the other 99% are still on the broken version. So for genuinely critical bugs, you can't rely on submission alone. You also need to "Remove from Sale" / pause the rollout in App Store Connect, and ship an OTA hotfix in parallel for the JavaScript layer where possible. This is the three-layer dance.
How to enable Phased Release
From the App Store Connect web UI:
App Store Connect → your app → "App Store" tab
Select the version you're submitting
Under "Phased Release for Automatic Updates", check "Release update over a 7-day period using phased release"
Save → Submit for Review
I avoid the per-release manual click by scripting this with fastlane's deliver action, which I cover in detail in the Rork × Fastlane × EAS release automation guide — the same script can toggle Phased Release on automatically, so you don't have to remember the checkbox.
Layer 1: Using Android Staged Rollout correctly
Android's Staged Rollout is finer-grained than iOS. From the Play Console you choose any percentage (1%, 5%, 10%, 25%, 50%, 100%, or a custom number) and advance to the next stage on your own schedule.
My default cadence: ship at 5%, advance to 25% after 24 hours of clean signals, 50% another 24 hours later, 100% at 72 hours total. Unlike iOS's fixed 7 days, Android lets you sync rollout to your own life rhythm.
Pitfall: rolling back means shipping forward
On Android you can't actually "revert active users to the old version." Even mid-rollout, getting users back to a previous release means building a new APK/AAB with a higher version code that contains the old behavior and submitting that as the next release.
If you're using EAS Build or Rork's automatic build, this means combining expo-updates rollback with rebuilding from an earlier git commit and bumping the version code. It's worth simulating the procedure once when nothing's on fire — I rehearse a "rollback drill" on every new app I publish, because the first time I tried it under pressure I made mistakes that doubled the recovery time.
Layer 2: Auto-monitoring crash rate with Sentry
A staged rollout is meaningless if you don't notice the bugs landing. My setup uses the Sentry SDK to track per-release Crash-Free Sessions Rate.
Minimal Sentry integration for a Rork app
// In app/_layout.tsx or index.tsimport * as Sentry from "@sentry/react-native";import Constants from "expo-constants";Sentry.init({ dsn: process.env.EXPO_PUBLIC_SENTRY_DSN, // The release identifier is the linchpin: bake "appName@version+buildNumber" // so you can compare crash-free rates per release as Phased Release advances release: `${Constants.expoConfig?.name}@${Constants.expoConfig?.version}+${Constants.expoConfig?.ios?.buildNumber ?? "unknown"}`, // Production only — don't pollute the dashboard with experimental crashes enabled: !__DEV__, // Sample to keep volume manageable tracesSampleRate: 0.1, // Don't ship PII (a sensible default for indie developers) sendDefaultPii: false,});
Setting release matters more than people realize. Without it, when v1.4.3 ships during v1.4.2's Phased Release, the Sentry dashboard mixes both versions' numbers and you can't tell which is misbehaving. With it, you see "v1.4.2 is crash-free 99.8%, v1.4.3 is 97.2%" at a glance.
A GitHub Actions watcher that pages Slack on threshold breaks
Watching the Sentry dashboard 24/7 isn't realistic. I poll the crash-free rate every 15 minutes from GitHub Actions and post to Slack only when it drops below threshold.
The threshold (0.995 above) is calibrated to your app's baseline. My apps run at 99.8–99.9% in steady state, so 99.5% is "this is unusual, look now." The right move is to run this in shadow mode for a week — log alerts to a channel without paging anyone — and tune the number until false positives are rare.
Layer 2: The five numeric lines I use to decide "pause or continue"
When the alert fires, I use these as starting-line decision criteria. They're tuned for solo or small-team apps; large enterprises use different numbers.
Crash-free rate dropped 0.3 points or more from baseline. Pause immediately. Example: baseline 99.8% → at 99.5% I look hard, at 99.0% I pause.
The same stack trace exceeds 50 occurrences in one hour. Pause immediately. Almost always a release-induced regression.
Five or more one-star reviews in 24 hours, all citing the new version. Pause to protect the store ranking signal.
DAU dropped 10% week-over-week. Indicates users may not be opening the app — investigate before deciding.
Crashes concentrated on a single OS version. Usually I don't pause; instead I ship a targeted fix and let the rest of the rollout continue.
These are guidelines, not absolute rules. The real value is having a pre-decided framework — when an alert hits at 2am, you don't want to be reasoning from scratch.
Layer 3: Choosing the recovery path — OTA or Expedited Review?
When a crash is found, your first decision is: JavaScript/TypeScript layer, or native layer? That single classification determines which recovery tool you can use.
JS/TypeScript layer → ship via EAS Update OTA
Rork output is a React Native + Expo project, so if expo-updates is wired in, you can replace the JS bundle and assets without going through review. If you haven't wired it in yet, the EAS Update implementation guide for Rork apps covers the prerequisites and configuration; do that first, then come back here.
# 1. Commit the fixgit add .git commit -m "fix: handle null user in profile screen"# 2. Publish via EAS Update (production channel)eas update --branch production --message "Hotfix: profile crash on null user"# 3. Optionally pin to a specific runtime version# Only clients matching app.json's runtimeVersion receive the update
The crucial constraint: changes that touch native code can't ship over the air. These all require a native rebuild:
Bundle identifier changes in app.json
Adding a new native module (e.g., installing react-native-vision-camera for the first time)
Adding a key to Info.plist like NSPhotoLibraryUsageDescription
Permission changes in AndroidManifest.xml
Any modification inside Swift/Kotlin native modules
For these, you go to Expedited Review.
Native layer → request Expedited Review
Apple's Expedited Review is a request channel for urgent bug fixes. In my experience, expedited submissions are reviewed within 2–6 hours (vs. 24–48 for standard review). The catch: overusing it lowers your future approval odds, so save it for genuinely critical situations.
Approved expedited requests share a pattern. Here's the template I use:
Subject: Expedited App Review Request — [App Name] v[X.X.X]
Hello App Review Team,
We are requesting expedited review for [App Name] (App Store Connect ID: [ID]).
Issue summary:
After releasing v[X.X.X] on [date], approximately [N]% of users on iOS [version range]
experience a crash on launch caused by [specific cause: e.g., null reference in Core Data
migration]. Crash-free rate dropped from 99.8% to 97.1% in 12 hours.
Why this needs expedited review:
- The issue affects users on launch, so they cannot use the app at all.
- Approximately [N] users are blocked daily based on our crash analytics.
- The fix is isolated to [specific file], with no other behavior changes.
Mitigation already taken:
- Phased Release was paused at [date] when crash rate exceeded threshold.
- This submission contains only the fix; no new features.
Submission details:
- Build number: [X.X.X (Y)]
- Changes from previous version: [1-2 sentences describing the diff]
Thank you for your consideration.
[Your name]
Three things make this work: (1) state the impact in concrete numbers, (2) make it clear you've already paused the Phased Release so the bug isn't growing, and (3) demonstrate the fix is minimal and side-effect-free. Doing this consistently brought my approval rate above 80%.
Android: review is fast enough that expedited isn't a concept
Google Play doesn't have an explicit "expedited" channel because regular review is already 1–3 hours. The thing to focus on for Android is the rollout percentage — pause the staged rollout, submit a fixed build with a new version code, and resume from a low percentage when ready.
A worked example: calibrating the threshold for a real app
Let me walk through how I calibrated the crash-free threshold for one of my live apps, because the abstract advice ("tune to your baseline") only becomes concrete with numbers.
The app is a wallpaper utility with about 80,000 DAU and a baseline crash-free rate of 99.85% (averaged over a quiet two-week stretch). My initial threshold was 99.5% — a 0.35-point drop. In the first two weeks of running the GitHub Actions monitor, I logged every alert without paging Slack:
Day 3: alert fired briefly to 99.42%, recovered to 99.81% within an hour. Cause: a single device-specific WebView crash that happened in clusters and self-resolved. False positive for me — single-device clusters aren't actionable.
Day 7: alert fired to 99.31% sustained for two hours. Cause: a real bug introduced in v2.1.0 affecting iPad on iOS 16.x. True positive — I would have wanted to know about this immediately.
Day 11: alert fired to 99.48% briefly. Cause: noise from a small cohort of devices on a specific carrier. False positive.
After two weeks of shadow mode, I tightened the threshold to 99.3% and added a "sustained for 30 minutes or more" condition. That suppressed the noise while still catching the iPad-on-iOS-16 type of regression. The lesson is that the right threshold isn't a number you can guess — it's a number you find by running the system and labelling its alerts for a couple of weeks before it's allowed to wake you up.
If you have less DAU and fewer crashes, you may need to use absolute crash counts rather than rates because the rate is too noisy. For an app under 1,000 DAU, "more than 5 crashes in an hour from the latest release" tends to be a more stable trigger than any percentage.
The first-twelve-hours timeline I follow
When a new version starts its Phased Release, here's the rhythm I keep — built into a calendar reminder so it doesn't depend on willpower.
T+0 (release goes live): Check Phased Release toggle is on, Staged Rollout is at 5%, and Sentry is showing the new release identifier in the dashboard. If not, fix it now, while the audience is still 1%.
T+1 hour: First crash-free check. New releases sometimes show 100% just because no crashes have arrived yet — what I'm looking for is "session count is non-zero and crash-free is at baseline."
T+3 hours: Check the App Store and Play Store review feed. New one-stars in the first three hours about behavior that worked before are the clearest possible signal.
T+6 hours: Compare crash-free between v[new] and v[previous]. If [new] is 0.2 points or more below [previous], pause and investigate.
T+12 hours: If everything looks green, advance Android Staged Rollout from 5% to 25%. iOS is on its automatic schedule.
T+24 hours: Advance Android to 50% if green. Check whether App Privacy Report flagged any new tracking domains.
T+72 hours: Advance Android to 100%. Write the release retrospective if anything was unusual.
The point of writing this rhythm down is the same as the runbook: removing decisions from the moment when you're least equipped to make them.
When OTA and Expedited Review need to be coordinated
The two recovery paths sometimes need to be used together, not as alternatives. The case I see most often: the JavaScript layer can be patched immediately, but the underlying issue revealed a missing native permission or capability that should also be fixed properly.
In that situation I follow this sequence:
Ship the OTA fix immediately to stop the bleeding. This usually goes live within minutes for production-channel users.
Submit a native build with the proper fix the same day, with Phased Release disabled on the hotfix submission so it reaches everyone in 24–48 hours when approved.
Request Expedited Review only if Layer 2 monitoring still shows degraded numbers after the OTA — which can happen if the OTA only partially works.
The mistake to avoid is shipping the OTA, declaring victory, and then being surprised three days later when the next regular release reintroduces the bug because you never fixed the native layer underneath. The OTA is a tourniquet, not a cure.
The hotfix runbook
Here's the checklist I've literally taped to the wall next to my desk. The point of writing it down is that at 2am, with a phone full of alerts, I shouldn't be remembering procedures from scratch.
[ ] Categorize the crash: native or JS?
[ ] Note affected user count and version range in Sentry
[ ] Pause Phased Release in App Store Connect
[ ] Lower or pause the Staged Rollout in Play Console
[ ] Reproduce the bug locally
[ ] Confirm the fix removes the repro
[ ] JS-only? → eas update --branch production
[ ] Native? → bump build number, submit, request Expedited Review
[ ] Reply to the affected reviewers in the store after the fix is live
[ ] Write a post-mortem in Notion/issue tracker (so this exact bug can't recur)
The list isn't sophisticated, but its job is to work when I'm half awake. "When something breaks, do these in order" — that's the actual fastest recovery path.
Rollback strategy: returning fully to the previous version
When neither OTA nor expedited review is enough — say you shipped a database migration that corrupts existing user data — you go to a full rollback.
iOS doesn't have a "re-release a previously approved version" button. The procedure is:
Reset the git working tree to the commit before the bad release
Bump both the marketing version and the build number (e.g., 1.4.3 → 1.4.4)
Note in the release notes that this is functionally equivalent to a previous version
Submit for review (request expedited review if appropriate)
Android follows the same shape, but Play Console's Internal App Sharing lets you side-load the rollback build to your own device first to confirm behavior. That's a small luxury iOS doesn't have.
Plan on doing this once a year as an operational baseline. I keep an app.json version-bump script and a GitHub Actions workflow that builds from any release tag, so when the moment comes I'm not improvising. Improvising under pressure is when the second outage happens.
A note on TestFlight and Internal Testing as an extra layer (optional fourth)
This article focuses on the three production layers, but I want to mention a fourth that some of you may already have. Apple's TestFlight (especially the public link variant) and Google's Internal/Closed Testing tracks can act as a "Layer 0" — releasing to a small dedicated cohort before the production submission even happens.
I treat this as optional rather than required for indie apps because maintaining a beta cohort takes effort: you need testers willing to use pre-release builds, and you need a way to triage their feedback faster than your release cadence. For apps where I have an active Discord community of 50+ engaged users, I run TestFlight as Layer 0. For apps that are mostly anonymous downloads from search, I skip it because the cost-benefit doesn't work — the testers I'd recruit aren't representative of my actual users.
If you have a community, add it. If you don't, the three production layers in this article are enough on their own.
Why staged rollout alone isn't enough — the design philosophy
If you've followed along, you've probably noticed: Phased Release and Staged Rollout cap day-one impact at 1–10%, but they don't detect problems for you. They are time-buying mechanisms, not decision mechanisms.
The decision still has to be made by a human (you), which is why Layers 2 and 3 aren't optional — they're what turn the bought time into actual recovery. Skip any of the three layers and the difficulty of running production goes up sharply.
I think of this as "build a bridge that's easy to repair when it breaks," not "build a bridge that won't break." Aiming for unbreakability is more expensive in the long run than designing for graceful repair.
Common failures and pitfalls
Things I've personally messed up, or that come up often when other indie devs ask me about production:
Submitting an emergency fix without pausing Phased Release first. The fix's own Phased Release starts over from 1%, slowing recovery. Either pause the active release before submitting, or disable Phased Release entirely on the hotfix submission.
Forgetting to set Sentry's release field. Your dashboard mixes versions and you can't tell which one is broken. Set it once, never miss it.
Trying to OTA-ship a native change. The bundle is delivered, but the runtime integrity check at launch fails and crashes increase. Whenever you change app.json, rebuild natively. If your OTA appears to ship but never lands on devices, the EAS Update troubleshooting guide covers the five most common causes.
Threshold too tight, alerts too noisy. You stop reading the alerts, then you miss the real one. Loosen for the first month, tighten as you learn the baseline.
Practicing rollback for the first time in production. You'll introduce new bugs while panicking. Run the drill once in a sandbox app first.
What changes once you've shipped a few releases this way
The first time you set up the three-layer net, every release feels heavy because you're checking everything manually. By the third or fourth release, the rhythm becomes automatic: open Sentry once an hour for the first few hours, glance at the review feed once, advance Android in the evening if green, sleep. The cognitive load drops to a fraction of what it was when I was hand-watching every release.
This compounds in another way too. Because the recovery procedures are documented and rehearsed, my willingness to ship grows. I'll release a feature on a Thursday evening that I would not have shipped before this system was in place — because I now know I can stop the bleeding within minutes if something goes wrong. Faster shipping is the second-order benefit that most articles about reliability skip over: the point of safety isn't to ship less, it's to ship more confidently.
For Rork in particular, where AI-generated code can occasionally introduce subtle issues that don't appear in your manual testing, having this safety net is what makes "trust the AI's code in production" practically possible. Without it, you'd either be paranoid about every release or careless about a serious one. With it, you're somewhere stable in between — generating fast, validating gradually, and recovering quickly when needed.
What to do next: one thing
If this article is going to be useful to you, the highest-leverage action is just one thing:
Open App Store Connect and Play Console for the app you're running today, and confirm Phased Release / Staged Rollout is enabled. That's it. Nothing else in this article matters as much as that one toggle, and it's a few clicks of work.
Once that's locked in, spend a weekend on Layer 2 (Sentry + alerting) and Layer 3 (the OTA and expedited-review runbooks). When all three layers are in place, your heart rate during a release drops noticeably. Personally, I no longer wake up at night to check whether the store reviews have caught fire — and that's mostly because of this exact setup.
Thank you for reading this far. Wishing you long, safe, and quietly successful releases ahead.
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.