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.
New apps already work. To control updates from Dart in an existing app, add the package:
flutter pub add quickpatch_code_push
With auto_update: true (default) updates download automatically on launch - no Dart code needed. Add the package only for manual control:
import 'package:quickpatch_code_push/quickpatch_code_push.dart';
final _updater = QuickPatchUpdater();
void main() async {
// Check for update in background - non-blocking
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%.
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-derived