</>CodeWithKarani

Flutter Over-the-Air Updates Without App Store Review: What's Actually Possible

Karani GeoffreyKarani Geoffrey9 min read

Someone on your team shipped a typo in a price. Or a promo banner points at the wrong campaign. Or a backend field got renamed and the app now shows a blank screen on launch. Whatever it is, it is live, it is embarrassing, and the fix is a one-line change. On the web you would have it deployed in ten minutes. On Flutter you are staring down an App Store review queue that might take a day, and a rollout that reaches users whenever they next update.

So you go looking for the Flutter equivalent of React Native's old CodePush, and you find a GitHub issue that has been open for years with hundreds of comments, no official answer, and a pile of links to commercial services. Nobody explains what those services actually do, whether Apple will let you keep doing it, or how much of this you could just build yourself for the boring 90% of cases that are content and config, not code.

That explanation is this article. The short version is that there are two very different things people lump together as "OTA updates", and only one of them is a policy minefield.

separate updating data from updating code. They are not the same problem.

  • Content and config (text, prices, feature flags, layout driven by data, remote JSON) can be updated any time, with no store review, and this is fully within Apple and Google policy. Build this yourself; it is a remote config fetch.
  • Executable Dart code updates are what CodePush did and what commercial Flutter services (Shorebird and similar) do via patching the compiled artefact. This lives in a policy grey area on iOS and can move.
  • Design your app so the fixes you actually need at 2am are data fixes. Then you almost never need the risky path.

Why Flutter has no first-party CodePush

React Native's CodePush worked because a React Native app is mostly a JavaScript bundle interpreted at runtime. Apple's guidelines have long carved out an exception for interpreted code: you may download and run JavaScript in the system WebView or JavaScriptCore, as long as it does not change the app's primary purpose. CodePush shipped a new JS bundle into that exception. Microsoft has since retired the classic CodePush service, which is worth remembering before you treat the React Native story as the settled happy path; it was never as permanent as it looked.

Flutter is a different animal. In release mode on iOS, your Dart is compiled ahead of time to native ARM machine code and linked into the app binary. There is no interpreter sitting there waiting for a new bundle. To change the code you would have to change compiled machine code inside a shipped app, and that runs straight into Apple's rule that an app may not download executable code that changes its behaviour in ways not reviewed. That is the core reason there is no blessed first-party mechanism: the thing you want is exactly the thing the platform is built to prevent.

The OTA spectrum, from routine to risky Content + config prices, copy, banners feature flags, remote JSON No review. Fully allowed. Data-driven UI screens described by data server-driven layout Allowed. More engineering. Patching Dart code changing app behaviour via compiled patches Grey area on iOS. design so your emergency fixes live on the left and you rarely need the right
Most "we need OTA" panics are content problems wearing a code-problem costume.

What the commercial OTA services actually do under the hood

The Flutter-specific services that offer real code updates do not ship you a new Dart source file. They work at the level of the compiled artefact. In broad strokes: they build your release the way Flutter's toolchain does, capture the compiled output, and when you push an update they compute a binary patch against the version already installed on the device. The app downloads that patch, applies it to what it has, and the Dart runtime runs the patched result. It is closer to a delta update of the compiled app than to swapping a script.

You do not need to understand the internals to reason about the risk, and you should not pretend to more precision than you have about a proprietary system. The two things that matter are: it is genuinely changing executed code, not just data; and it depends on the specific way the current Flutter engine builds and loads that code, which is why these services track Flutter releases closely and occasionally lag a new version. Both of those are the reasons the policy question is live rather than settled.

The app-store policy risk, stated plainly

On iOS, Apple's review guidelines restrict downloading code that changes an app's features or functionality after review. There is an interpreted-code exception, and there is enforcement that has historically been uneven. What that adds up to for you, as an engineer making a decision, is not a clean yes or no. It is a risk you are choosing to carry:

  • The mechanism can work fine for a long time and then be flagged in a future review, especially if a reviewer notices behaviour that was not in the reviewed build.
  • The rules and their enforcement can change without notice, and a service that is compliant today can be non-compliant after a guideline update.
  • The blast radius of getting this wrong is your entire iOS presence, because the penalty is app rejection or removal, not a warning.

Google Play is more permissive about delivering code post-install, so the sharper edge is almost always iOS. None of this means "never use a code-OTA service". It means treat it as a deliberate risk decision with a named owner, not a convenience you reach for because a review is slow. If you are already dealing with review rejections, the patterns in Guideline 2.3.1 rejections are the same muscle: assume the reviewer will eventually look closely.

The DIY approach for the 90% that is not code

Here is the part almost nobody writes down: the overwhelming majority of "we need to push an update right now" moments are data, and data OTA is trivial, free and completely within policy. You build it once.

Step 1: Move anything that changes out of the binary

Prices, promotional copy, banner targets, feature flags, opening hours, support phone numbers, the ordering of a home screen: none of this should be hardcoded in Dart. Put it behind a remote source. That can be a managed remote-config product or a single JSON file on a CDN or your own API. The rule is simple: if a non-engineer might need it changed in a hurry, it is data, and data does not belong in a compiled binary.

Step 2: Fetch config on launch, with a bundled fallback

On startup, fetch the remote config, cache it locally, and ship a sensible default compiled into the app so the first launch and the offline case both work. A minimal shape:

Future<AppConfig> loadConfig() async {
  try {
    final res = await http
        .get(Uri.parse('https://cdn.example.com/app-config.json'))
        .timeout(const Duration(seconds: 4));
    if (res.statusCode == 200) {
      final cfg = AppConfig.fromJson(jsonDecode(res.body));
      await _cache.write(res.body);   // survive the next offline launch
      return cfg;
    }
  } catch (_) {
    // network down, bad response: fall through to cache, then to bundled default
  }
  final cached = await _cache.read();
  if (cached != null) return AppConfig.fromJson(jsonDecode(cached));
  return AppConfig.bundledDefault();
}

This one function is the difference between "we ship a hotfix through review" and "someone edits a JSON file and it is live in minutes". It never downloads code, so there is no policy question at all.

Step 3: Add a remote kill switch and a soft update prompt

Two more fields in that same config file cover most real emergencies without a store release:

  • A kill switch per feature, so a broken feature can be turned off remotely while you prepare a real fix. A blank screen becomes a hidden menu item, not a crash.
  • A minimum supported version and a message, so when you truly must force everyone onto a new store build, the current app can detect it is too old and show a "please update" screen rather than misbehaving. This does not update the app; it tells the user to, which is entirely allowed.

Step 4: Consider server-driven UI only if you genuinely need it

If you need to change not just values but which widgets appear and in what order, you can describe screens as data and render them with a fixed set of Dart widgets. This is real engineering and easy to over-invest in, but it is policy-clean, because you are shipping data that your reviewed code interprets, not new code. Reach for it when layout changes are frequent, not to future-proof against a hypothetical.

What genuinely requires a store release, and what does not

ChangeNeeds a store release?Why
Fix a wrong price or typoNoIt is data; serve it from remote config
Turn off a broken featureNoRemote kill switch flag
Change a home screen's contents or orderNo, if data-drivenServer-driven data your reviewed code renders
Force users onto a newer buildThe new build does; the prompt does notMin-version check shows a prompt, cannot install the update itself
Fix a Dart logic bug in existing codeYes (or accept code-OTA risk)Compiled code cannot be changed within policy on iOS
Add a new native plugin or permissionYesNative binary and entitlements are set at build and review time
Ship a new screen made of new widgetsYesNew compiled code, unless expressible in your server-driven schema

What people get wrong

Treating code-OTA as a normal part of the release process. Some teams start pushing code patches for routine changes because it is faster than review. Every one of those patches is a change to your app that Apple did not review, and you are accumulating risk on your most important platform to save a day. Use the store for code. Use OTA for data.

Assuming React Native solved this and Flutter is behind. The classic CodePush service has been retired, and even at its peak it relied on the interpreted-code exception that Flutter's compiled model does not have. This is not a gap Flutter forgot to fill; it is a consequence of AOT compilation, which is also why Flutter is fast. The trade-off is real in both directions.

Hardcoding config "for now". The single most common cause of a needless emergency release is a value that lived in Dart because wiring up remote config felt like overkill at the time. It is not overkill. It is the cheapest insurance in mobile, and step 2 above is the entire implementation.

Picking a commercial OTA provider on features alone. If you do adopt one, the questions that matter are not about the demo. Ask how quickly they support each new Flutter release, what happens to already-deployed patches if you upgrade Flutter, exactly which store policies they claim compliance with and who carries the risk if a review flags it, and how you roll a bad patch back. A provider that cannot answer the rollback and Flutter-version questions crisply is one you will regret during an incident.

When you still think you need code OTA

  • The "bug" is really unhandled bad data. A surprising share of crashes that feel like code bugs are the app choking on a backend response it did not expect. Fix the backend, or make the client tolerant of the bad shape via config, and you have fixed it OTA without touching code.
  • You need faster review, not OTA. Apple's expedited review exists for genuine emergencies. It is not instant and you cannot lean on it weekly, but for a real production incident it is often faster than integrating an OTA system you do not already have.
  • You are shipping frequent logic changes. If your app legitimately needs new code weekly, that is a signal to move more logic server-side, behind your own API, where you deploy freely. The thinner the client, the less you ever need to patch it.
  • You have weighed the risk and still want it. Then adopt a code-OTA service as an explicit, owned decision, keep it for hotfixes rather than routine releases, and make sure someone is accountable for tracking Apple's policy over time. Going in with eyes open is a defensible engineering call; sleepwalking into it is not.

The reframe that ends most of these debates: you do not have an "OTA update" problem, you have a "too much of my app is baked into a binary that takes a day to change" problem. Move the volatile parts out into data you control, and the store review stops being on your critical path for everything except actual new code.

Frequently asked questions

Does Flutter have a CodePush equivalent for over-the-air updates?
There is no first-party equivalent, because in release mode Flutter compiles Dart ahead of time to native machine code with no runtime interpreter to swap a bundle into. React Native's CodePush relied on Apple's interpreted-code exception for JavaScript, which Flutter's compiled model does not have. Commercial services offer real code OTA by patching the compiled artefact, which carries policy risk on iOS, while content and config updates can be done freely yourself.
Can I update my Flutter app's content without an App Store review?
Yes, and it is fully within Apple and Google policy. Anything that is data (prices, copy, banners, feature flags, layout described by data) can be served from remote config or a JSON file on a CDN and updated any time. Fetch it on launch, cache it, and ship a bundled default for offline. No review is involved because you are never downloading executable code.
Is it against App Store rules to push code updates to a Flutter app?
On iOS it sits in a grey area. Apple's guidelines restrict downloading code that changes an app's behaviour after review, and enforcement has been uneven. A code-OTA service can work for a long time and then be flagged, and the rules can change. Treat it as a deliberate, owned risk decision used for hotfixes, not as a routine part of your release process. Google Play is more permissive.
What is the simplest way to fix a wrong price in a live Flutter app fast?
Serve the price from remote config rather than hardcoding it in Dart. Fetch a small JSON file on launch, cache it, and fall back to a bundled default. Then fixing a wrong price is editing that JSON file, live in minutes, with no store review. Most 'we need OTA right now' emergencies are data problems that this one pattern solves.
#Flutter#OTA Updates#App Store#Remote Config#Mobile
Keep reading

Related articles