Setup Guide
Add over-the-air code-push to any Flutter app in minutes. No App Store review for Dart code changes.
dart --version should work in your terminal)Get your API key
Sign in with Google on the dashboard. An API key (qp_api_…) is issued automatically - find it in the API Keys tab. You'll paste it into the CLI in step 2.
Pick whichever you prefer - both install the same CLI.
Option A - pub.dev (requires the Dart SDK)
dart pub global activate quickpatch_cli
Make sure ~/.pub-cache/bin is on your PATH (Dart usually adds this automatically). Open a new terminal, then run quickpatch --version to confirm.
Option B - standalone installer (macOS / Linux)
curl -fsSL https://raw.githubusercontent.com/letssuhail/quickpatch-cli/main/install/install.sh | bash
Windows (PowerShell): irm https://raw.githubusercontent.com/letssuhail/quickpatch-cli/main/install/install.ps1 | iex. Installs into ~/.quickpatch; add ~/.quickpatch/bin to your PATH (the installer prints the line). Update later with quickpatch upgrade.
Prefer a pre-built binary? Download it from the GitHub Releases page.
Authenticate with your API key from the dashboard:
quickpatch login
Paste your qp_api_… key when prompted. Credentials are saved to disk so you only need to do this once per machine.
Then set your server URL - macOS / Linux
export QUICKPATCH_HOSTED_URL="https://api.quickpatch.dev"
Add to ~/.zshrc or ~/.bashrc to make it permanent, then source ~/.zshrc.
Windows (PowerShell)
$env:QUICKPATCH_HOSTED_URL = "https://api.quickpatch.dev"
To make it permanent: setx QUICKPATCH_HOSTED_URL "https://api.quickpatch.dev" (then reopen the terminal).
QUICKPATCH_HOSTED_URL is reused automatically for engine downloads - no other URL needed.Inside your Flutter project folder:
cd your_flutter_app/ quickpatch init
Registers the app on your server and writes quickpatch.yaml (app ID + server URL). Commit this file.
base_url is filled in automatically from QUICKPATCH_HOSTED_URL - it tells your users' apps where to fetch patches.
auto_update: true (the default in quickpatch.yaml), patches download in the background and apply on the next launch automatically - no package and no Dart code. Skip this step unless you want manual control.Add quickpatch_code_push only when you want to drive updates from your own code - for example to:
auto_update: false) and manage everything yourselfIf you do want it, add the package:
flutter pub add quickpatch_code_push
import 'package:quickpatch_code_push/quickpatch_code_push.dart';
final _updater = QuickPatchUpdater();
void main() async {
// Manual control - only needed if you added the package.
final status = await _updater.checkForUpdate();
if (status == UpdateStatus.outdated) {
await _updater.update();
}
runApp(const MyApp());
}From your project folder (with the two env vars set):
quickpatch release android
Produces build/app/outputs/bundle/release/app-release.aab and registers release 1.0.0+1 on the server. First build downloads the engine once; later builds are fast.
quickpatch release android - NOT flutter build appbundle. A plain build is not registered, so patches won't work.Upload the same .aab to the Google Play Console as you normally would. The QuickPatch engine is bundled inside, so every install can receive OTA patches.
An .aab can't install directly - convert it to an APK with bundletool (shipped with the CLI):
export ANDROID_HOME="$HOME/Library/Android/sdk" export PATH="$ANDROID_HOME/platform-tools:$PATH" BT="$HOME/.quickpatch/bin/cache/artifacts/bundletool/bundletool.jar" cd build/app/outputs/bundle/release java -jar "$BT" build-apks \ --bundle=app-release.aab \ --output=app-release.apks \ --mode=universal --overwrite java -jar "$BT" install-apks --apks=app-release.apks
If you see "Unable to determine the location of ADB", append --adb="$HOME/Library/Android/sdk/platform-tools/adb". Launch the app once so it registers.
Edit any Dart file (e.g. text in lib/main.dart). Then publish the patch - run it without a version and the CLI lists your releases to pick from:
quickpatch patch android
…or target a release explicitly (best for scripts / CI, where there's no prompt):
quickpatch patch android --release-version=1.0.0+1
--release-version for an interactive pick from a list. Pass --release-version=1.0.0+1 (the shipped release's exact pubspec version) to target one directly, or --release-version=latest for the most recent. Auto-promoted to stable at 100%.Close and reopen the app twice:
Force-restart: adb shell am force-stop com.your.package, then relaunch.
--interpreter flag on both release and patch. It ships your Dart code as a bytecode module so patches can change code (not just data) over the air. The release and its patches must both use --interpreter.Recommended - build & codesign in one step (ready .ipa for App Store Connect / TestFlight):
quickpatch release ios --interpreter
Or build unsigned (sign later in Xcode - see step 2):
quickpatch release ios --interpreter --no-codesign
quickpatch release ios --interpreter - NOT flutter build ipa or a manual Xcode archive from scratch. Only quickpatch release registers the release, which patches require.If you used --no-codesign, QuickPatch produces an .xcarchive. Open it in Xcode (Window → Organizer) and distribute - with one critical setting:
If it stays checked, Xcode rewrites the build number, so the uploaded IPA no longer matches the version QuickPatch recorded - and patches will silently fail to apply.
So the Xcode-archive route works for patches as long as (a) the archive came from quickpatch release ios --interpreter, and (b) you uncheck that box. Using quickpatch release ios --interpreter (codesigned) skips this entirely.
Upload the codesigned .ipa to App Store Connect / TestFlight. The engine is bundled, so installs can receive OTA patches.
Install via Xcode or run on the simulator (no bundletool step on iOS). Launch once so it registers.
Edit any Dart file, then publish. Run it without a version and the CLI lists your releases to pick from:
quickpatch patch ios --interpreter
…or target a release explicitly (best for scripts / CI, where there's no prompt):
quickpatch patch ios --interpreter --release-version=1.0.0+1
--release-version for an interactive pick from a list. Pass --release-version=1.0.0+1 to target one directly, or --release-version=latest for the most recent. Always keep the same --interpreter flag you released with. Auto-promoted to stable at 100%.Close and reopen the app twice - launch 1 downloads, launch 2 boots into the patched code.
Edit any Dart file - UI, logic, a bug fix. No version bump needed for a patch.
Run it without a version and the CLI lists your releases to pick from (iOS adds --interpreter, matching the release):
quickpatch patch android # or quickpatch patch ios --interpreter
…or target the release explicitly (best for scripts / CI) with its pubspec.yaml version, e.g. --release-version=1.0.0+1 (or --release-version=latest):
quickpatch patch android --release-version=1.0.0+1 # or quickpatch patch ios --interpreter --release-version=1.0.0+1
QuickPatch diffs the new Dart snapshot against the release and uploads only the diff (~5-500 KB). Auto-promoted to stable at 100%.
On each launch the app checks your server and downloads any new patch in the background. It applies on the next launch - no App Store review, no reinstall.
- Dart logic & bug fixes
- UI changes, new screens, layout
- Text, colors, styling
- Business-logic changes
- Adding/removing Flutter plugins
- Native code (Kotlin / Swift)
- New bundled assets / fonts
- App version / SDK changes
When you change native code or dependencies, run quickpatch release again (and re-submit to the store). Patches carry only Dart code diffs.
By default a new patch goes to the stable channel at 100%. To stage it gradually, open the Rollouts tab in the dashboard:
If something is wrong, click Pause - devices stop receiving the patch immediately. Existing installs keep the last good patch.
A channel is a named audience for your patches, like stable or beta. A device only receives patches promoted to its channel, so you can prove a patch on a small beta audience before it reaches everyone. The channel is set by you, the app owner, in the build's config; your end users never choose it.
Every build is on stable unless you say otherwise. To run a beta program, ship a separate build pointed at the beta channel (for example your TestFlight or Play internal-testing build):
quickpatch init --channel=beta
This writes channel: beta into quickpatch.yaml. (You can also add that one line by hand to an existing config.)
Ship that build through TestFlight or Play internal testing as usual. Those devices now report the beta channel.
quickpatch patch android --release-version=1.0.0+1 --track=beta
Only beta devices receive it. Your store build stays on stable and is untouched.
quickpatch patch android --release-version=1.0.0+1 --track=stable
Channels control who gets a patch; rollout percentage controls how many of them. Combine both: e.g. beta at 100%, then stable ramped 10% → 100%.
Patch signing guarantees a device only applies a patch that was signed with your private key - so a patch can't be tampered with or spoofed in transit.
quickpatch release for an app, the CLI generates an RSA-2048 key pair, stores it locally, and embeds the public key in the build. Every later release and patch picks the keys up automatically - no flags, no openssl.Keys live at ~/Library/Application Support/quickpatch/keys/<app_id>/ (macOS) or ~/.config/quickpatch/keys/<app_id>/ (Linux). Back up private.pem - patches for releases built with that key can only ever be signed with it. To use the same key on another machine (or CI), copy the folder there.
Advanced: bring your own keys (CI / key vaults)
Explicit key flags always override the automatic keys.
openssl genpkey -algorithm RSA -out quickpatch_private.pem -pkeyopt rsa_keygen_bits:2048 openssl rsa -in quickpatch_private.pem -pubout -out quickpatch_public.pem
Keep quickpatch_private.pem secret (password manager or secrets vault). The public key is not sensitive - it gets embedded in your app.
quickpatch release android --public-key-path=quickpatch_public.pem # or quickpatch release ios --interpreter --public-key-path=quickpatch_public.pem
The public key is bundled into the app, so every install can verify patches. Do this on the build you ship to the stores.
quickpatch patch android --release-version=1.0.0+1 \ --private-key-path=quickpatch_private.pem --public-key-path=quickpatch_public.pem # or quickpatch patch ios --interpreter --release-version=1.0.0+1 \ --private-key-path=quickpatch_private.pem --public-key-path=quickpatch_public.pem
A patch signed with a key the app doesn't trust is rejected on device - it stays on the last good patch and never applies an unverified one.
QUICKPATCH_PUBLIC_KEYS to a comma-separated list of trusted public keys (base64) at release time, so the app accepts both the old and new key while installs migrate.quickpatch loginAuthenticate with your API key from the dashboardquickpatch logoutRemove stored credentialsquickpatch initRegister the app + write quickpatch.yamlquickpatch release androidBuild + publish a new Android releasequickpatch release ios --interpreter --no-codesignBuild + publish a new iOS release (code push)quickpatch patch android --release-version=1.0.0+1Publish an Android patch for that releasequickpatch patch ios --interpreter --release-version=1.0.0+1 --no-codesignPublish an iOS patch for that releasequickpatch releases listList all releases for the current appquickpatch patches listList all patches for the current appquickpatch upgradeShow how to upgrade to the latest versionQUICKPATCH_HOSTED_URLYour QuickPatch server URL (required)QUICKPATCH_TOKENAPI key override - optional if you ran quickpatch loginQUICKPATCH_STORAGE_BASE_URLEngine mirror override - optional, auto-derivedQUICKPATCH_PUBLIC_KEYSExtra trusted public keys (base64) for signing-key rotation - optional