DEV Community

Cover image for Flutter Over-the-Air Updates: A Complete Technical Guide to Code Push
Kumar Harsh
Kumar Harsh

Posted on

Flutter Over-the-Air Updates: A Complete Technical Guide to Code Push

A critical bug ships to production. Users report crashes in your payment flow. Your team has a fix committed and tested within the hour, then you file for App Store review and wait. Twenty-four to forty-eight hours on a good day, longer over a weekend. By the time the fix reaches users, retention has already taken a hit and your app store ratings have moved.

React Native teams sidestep this by swapping a JavaScript bundle. Flutter teams have historically had no equivalent option. This guide explains the architectural reason for that delta, how Shorebird's CodePush solves it at the runtime level, and how to get the full implementation running in your production pipeline.

Why Flutter Can't Do What React Native Does

Flutter compiles Dart code to native ARM machine code at build time using ahead-of-time (AOT) compilation. The output is a compiled snapshot baked directly into the app binary. At runtime, there's no interpreter between your Dart logic and the hardware.

React Native works differently. The JavaScript logic runs inside a JS engine (Hermes or JSC), loaded from a bundle file at startup. Expo's EAS Update ships a new bundle to devices over the air because swapping the JS file at the path the runtime reads from is all it takes. The app's logic layer is just a file on disk.

Flutter has no equivalent file to swap. The libapp.so on Android (and its iOS counterpart) is compiled Dart code in binary form. You can't diff a .dill snapshot against a new one and swap it at runtime the way you'd replace a JavaScript bundle. The runtime isn't built to load updated compiled code dynamically.

AOT compilation gives Flutter its consistent frame rate, fast startup, and predictable performance, and it also makes hot-patching hard.

Solving this requires either modifying the Dart runtime to support some form of dynamic code execution, or abandoning code changes entirely in favor of server-driven content. Shorebird chose the former: they modified both the Dart VM and the Flutter engine to make genuine diff-based OTA code updates possible on Android and iOS.

How Shorebird Code Push Actually Works

When you build a release with Shorebird, the resulting binary contains two components: the standard AOT-compiled Dart snapshot and a modified Dart interpreter that Shorebird built and maintains. On startup, the Shorebird runtime checks Shorebird's servers for a patch scoped to that specific release version. If no patch exists, the AOT snapshot runs exactly as it would in a standard Flutter build. If a patch is available, the runtime executes the updated Dart code through the interpreter instead, while the AOT snapshot remains in place as the fallback.

The system architecture documentation covers this in detail, but the operational model comes down to two concepts you need to keep distinct: releases and patches.

A release is a full app build that includes the Shorebird runtime. You build it with the Shorebird CLI, register it with Shorebird's servers, and submit the resulting artifact to the App Store and Google Play exactly as you would any standard build. Once users install it, that install is tied to a specific release version in Shorebird's system.

A patch is a Dart code diff computed against that release. Shorebird compares the new Dart snapshot against the original, produces a binary diff, and uploads only the changed bytes to its CDN.

Patches are version-scoped: a patch built against release 1.2.0 only applies to devices running 1.2.0. Users still on 1.1.0 won't receive it until you ship a patch targeting that release.

This model has a defined boundary, separating what you can update over the air from what requires a new store release:

Can update over the air Requires a new store release
All Dart and Flutter widget code Native Kotlin, Swift, Java, or Objective-C code
UI layout, business logic, routing Native plugin platform channel implementations
Strings, state management, providers AndroidManifest.xml or Info.plist changes
Dart-layer bug fixes New runtime permissions
Pure-Dart Flutter package upgrades Native binary assets
App configuration managed in Dart Changes to native dependencies

If the bug lives in a native plugin's Kotlin platform channel handler, a patch won't reach it. A new camera permission requires a new release. Anything outside pure Dart goes back through the store.

Setting Up Shorebird: From Init to First Patch

The setup from an existing Flutter project to your first deployed patch takes about 15 minutes.

Step 1: Install the Shorebird CLI.

Install the Shorebird CLI on your development machine:

curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/install/main/install.sh -sSf | bash
Enter fullscreen mode Exit fullscreen mode

Then authenticate with your Shorebird account:

shorebird login
Enter fullscreen mode Exit fullscreen mode

Step 2: Initialize Shorebird in your Flutter project.

shorebird init
Enter fullscreen mode Exit fullscreen mode

shorebird init adds a shorebird.yaml file at the project root containing your app_id, and configures the Flutter engine binding to include the Shorebird runtime. Your existing Dart code and project structure stay unchanged. The only new file tracked in version control is shorebird.yaml.

Step 3: Create a release.

Build your app with the Shorebird engine embedded using the platform-specific release commands:

shorebird release android
shorebird release ios
Enter fullscreen mode Exit fullscreen mode

These commands build your app with the Shorebird engine embedded, register the release with Shorebird's servers, and produce the artifacts you upload to the stores: an .aab for Play Console and an .ipa for App Store Connect. Your store submission workflow doesn't change.

Step 4: Submit to the stores through Play Console and App Store Connect as normal.

Use the .aab for Play Console and the .ipa for App Store Connect to publish your app. Once a version built with Shorebird is live, you're ready for over-the-air updates.

Step 5: Push a patch after fixing a bug in Dart code.

shorebird patch android
shorebird patch ios
Enter fullscreen mode Exit fullscreen mode

Shorebird diffs the new Dart snapshot against the registered release, uploads the delta, and makes it available immediately via its CDN. Users running that release version download the patch in the background on their next update check.

PushPress Senior Mobile Developer Allan Wright described the change in their release cycle: "Our speed to release updates has skyrocketed. It used to take four days to two weeks depending on urgency. Now it's way faster. The reliability of shipping fixes is night and day."

For teams running GitHub Actions, Shorebird provides documented CI integration that slots directly into existing workflows. Both shorebird release and shorebird patch run in CI with standard environment variables for authentication. Codemagic also has first-class Shorebird support for teams already using it as their build platform.

Controlling Update Behavior In-App

By default, patches download in the background and apply on the next cold start. For most bug fixes, this is the right behavior. Users get the fix without any interruption, and you avoid forcing a restart mid-session.

The shorebird_code_push Dart package gives you programmatic control when you need it. Add the dependency:

dependencies:
  shorebird_code_push: ^latest
Enter fullscreen mode Exit fullscreen mode

Then check for and download updates explicitly:

import 'package:shorebird_code_push/shorebird_code_push.dart';

Future<void> checkForUpdate() async {
    // Create an instance of the updater class
    final updater = ShorebirdUpdater();

    final status = await updater.checkForUpdate();

    if (status == UpdateStatus.outdated) {
      try {
        // Perform the update
        await updater.update();
      } on UpdateException catch (error) {
        // Handle any errors that occur while updating.
      }
}
Enter fullscreen mode Exit fullscreen mode

Call checkForUpdate() from the initState of your root widget, or from an AppLifecycleListener callback on app resume. The root widget approach catches users on every cold start; the lifecycle callback catches returning sessions.

For a patch addressing a security vulnerability or a crash hitting a significant fraction of your users, you can force a restart immediately after download. Wire this through PhoenixApp from the flutter_phoenix package or your own restart mechanism. Reserve forced restarts for genuine incidents, because most patches don't qualify and users will notice an unexpected restart.

The shorebird_code_push package also supports a customizable in-app update banner via _showUpdateAvailableBanner(). This notifies users that an update is available without interrupting their current session. They see a prompt, finish what they're doing, and restart at a natural break point. For anything short of a critical crash, this is the right default.

Advanced Patterns: Staged Rollouts, Rollbacks, Custom Tracks, and Patch Signing

High-velocity teams don't ship patches directly to 100% of users. WAGUS founder Michael Gallego described their patching cadence: "We patch fast. One release had 60 patches. Most of them are small quality-of-life updates requested by users. With Shorebird, we could ship them the same day." Shipping that volume safely requires a deliberate rollout strategy.

Percentage-based rollouts

Shorebird supports percentage-based rollout controls configured through the Shorebird console after you push a patch. A practical progression looks like this:

  • Start at 5% and monitor crash reporting (Crashlytics, Sentry, or your current stack) for 60 to 90 minutes
  • If error rates stay stable, expand to 25% and check again
  • Move to 100% after your second checkpoint passes cleanly

Starting at 5% limits your blast radius. If a patch introduces a regression your test suite missed, you catch it at 5% user exposure rather than after it's affected your entire active user base.

Rollbacks

When a patch causes a regression, run:

shorebird patch rollback
Enter fullscreen mode Exit fullscreen mode

Shorebird immediately reverts all users on that patch to the previous version, with no app store involvement and no review queue. The rollback documentation covers command syntax and the confirmation flow.

Rollback takes effect at the infrastructure level immediately. Users receive the previous patch the next time their app checks for updates, with no action required on their end.

Custom tracks

Shorebird supports named update tracks for staging and beta environments:

shorebird patch android --track=beta
Enter fullscreen mode Exit fullscreen mode

Beta testers receive the patch immediately while production users remain on the current stable version. This gives you a real staging-to-production pipeline for OTA patches, not just for store submissions. You can assign users to tracks through the Shorebird console and promote a patch from beta to stable once it's passed QA. The custom tracks documentation covers track configuration in full.

Patch signing

A compromised Shorebird account could push arbitrary Dart code to your production users without cryptographic patch signing. Patch signing requires you to generate and control a private key used to sign each patch. The Shorebird runtime verifies the signature on-device before applying any patch, and rejects the patch if verification fails.

The threat model this closes is account compromise at the Shorebird layer. TLS protects the delivery channel. Patch signing protects against the scenario where an attacker gains access to your Shorebird account credentials and attempts to push a malicious patch. For teams in fintech, healthcare, or any regulated industry where code changes require an audit trail, patch signing is the recommended baseline configuration.

App Store and Play Store Compliance

Shorebird's approach is compliant with both stores, with a defined boundary you need to respect.

Apple's App Store Review Guidelines section 2.5.2 prohibits apps from downloading and executing code that introduces or changes the app's features or functionality outside of the App Review process. The guideline includes a carve-out: apps may execute code that runs through a built-in interpreter, provided the code doesn't change the app's core purpose or add capabilities that would otherwise require App Review.

Shorebird's mechanism fits within this carve-out. Patched code runs through the Shorebird Dart interpreter. The patches fix existing Dart logic rather than introducing net-new capabilities. Shorebird maintains a documented compliance position covering the boundary in detail.

Use OTA patches for bug fixes, performance improvements, and iteration on existing features. Don't use OTA to ship a new payment screen or a user-facing feature that you deliberately omitted from your last App Review submission. That applies to any interpreted-code OTA mechanism on iOS, not just Shorebird.

Google Play's developer policies are more permissive. Play explicitly allows OTA code updates without the same interpreted-code carve-out requirement, because the platform doesn't impose the same restriction. Standard policy compliance applies (no loading code from untrusted external sources, no violations of content policies), but OTA patching itself carries no special compliance risk on Android.

If your team operates in a regulated industry, document each patch with a short description of what changed. The Shorebird console stores version history for every patch across every release. Pair that record with your existing change management process and you have the audit trail most compliance frameworks require.


Have you run into App Store review delays on a critical Flutter bug fix? Or already have Shorebird wired into your pipeline? Drop your experience in the comments, I'd genuinely like to know how other teams are handling this.

Top comments (0)