codepush 0.2.8
codepush: ^0.2.8 copied to clipboard
Ship Dart fixes to installed Flutter apps without an app-store review. Signed patches, staged rollout, crash-safe rollback. Android needs no forked engine.
codepush #
Ship Dart fixes to installed Flutter apps without an app-store review.
Your app gets one file and one dependency line. Everything else — which bucket serves your patches, which key your releases trust, what has already been released — is resolved from the app id in that file.
# codepush.yaml
app_id: cp_7f3a9c2e5b1d4860
# pubspec.yaml
dependencies:
codepush: ^0.1.0
That is the whole integration. No main.dart changes, no Android code, no
pubspec asset entry.
What a patch can change #
Dart. Not native code, not plugins, not assets, not permissions. A Dart-level bug fix or a Dart-level feature; anything else still needs a store build.
Android and iOS work differently #
Android replaces libapp.so — all of your compiled Dart. The stock Flutter
engine already knows how to boot from a different one, so Android needs no
forked engine. A patch is a few megabytes.
iOS cannot load native code that was not signed into the bundle, so a patch
there is Dart bytecode, run by the VM's interpreter, replacing function
bodies in place. That needs a Flutter engine built with
--dart-dynamic-modules, and the applier lives in a separate package:
dependencies:
codepush: ^0.1.0
codepush_ios: ^0.1.0 # only if you patch iOS
It is separate because it calls dart:_internal.applyCodePushPatch, which only
the forked engine has — and a direct call to it fails to compile against a
stock SDK. Keeping it out of this package is what lets an Android-only app, and
every developer's debug build, use a stock Flutter.
Every patch is signed #
The signing key is generated on your machine and never leaves it. The server holds only the public half, and your app has that half compiled in.
So a compromised server can withhold a patch or serve an old one. It cannot forge one — the signature will not verify against the key baked into the binary. The same is true of a stolen API key.
A device checks, in this order: signature, then app id, then revocation, then engine, then release, then rollout, then the artifact hash. The signature is first because every check after it reads a field out of the manifest, and those are attacker-controlled until it passes.
If a patch breaks something #
A patch is only ever attempted. It stays live only if the app reaches a first frame; a crash before that quarantines it on the next launch, with no server involved.
You can also stop one everywhere:
codepush patches revoke 7 --reason "crash loop on Android 12"
Devices running it return to the shipped build on their next cold start. This is the one command that needs no signing key — requiring one to stop a bad patch would mean being unable to stop it from a machine that lacks it.
Showing a download #
Optional, and off by default. Add the strip if you want one:
Stack(children: [child, const CodePushStatusStrip(bottomOffset: 92)])
It appears while a patch downloads and disappears when it is done. It says nothing on success, deliberately: the patch takes effect the next time the app is opened, so "update ready" would be an interruption offering no action.
Getting started #
Install the CLI and register the app:
dart pub global activate codepush_cli
codepush login --token <key from your dashboard>
codepush init
codepush keys generate # before your first release, always
codepush release android
See codepush_cli for the full
workflow.