How It Works
A look under the hood at VM Patches — how a Dart fix on your machine becomes verified, running code on a user's phone. (For Runtime Bundles, the mechanics are simpler and covered on the Runtime Bundles page.)
The three pieces
VM code push involves three components working together:
- The Koolbase engine. A build of the Flutter engine that can verify and apply patches at launch. Your app must be built with it. A staged patch is verified and applied before your app's code runs — on iOS, before the first frame renders.
- The SDK (
KoolbaseVmPatchClient). Runs inside your app. It asks the server whether a patch exists for the running binary, downloads it, and stages it in the directory the engine reads from. - The server + CLI. You build a patch with the CLI; it's signed, uploaded, and registered against a release. The resolver decides which patch a given device should get.
The end-to-end flow
- You fix a bug in Dart and recompile your app binary.
- The CLI produces a signed patch and uploads it. On Android it diffs your recompiled binary against the live build; on iOS it compiles your changed Dart source into a compact bytecode patch (
patch ios build+patch push-ios). - On a user's device, the SDK calls the resolver on launch, sending the running build's identity.
- The resolver returns the matching patch; the SDK downloads it and stages it in the engine's patch directory.
- On the next cold launch, the engine finds the staged patch, verifies it, applies it, and your fixed code runs.
Why the next launch, not this one
Full patches vs diff patches
A patch carries the new compiled Dart. On Android it can do that in two ways:
- Full patch. Carries the entire new code blob. Simple, but large (megabytes).
- Diff patch (
--diff). Carries only a binary delta against the installed build. The engine reconstructs the full new blob on-device from the installed bytes plus the delta. Typically tens of kilobytes — dramatically smaller and faster to deliver.
Prefer diff patches
On iOS, a patch is a compiled-bytecode update to the functions you declared patchable — naturally compact (typically under a kilobyte for a targeted fix), signed, and pinned to the exact binary like everything else. There is no full-vs-diff choice to make; the artifact is always minimal.
build_id: binding a patch to a binary
Every Koolbase build is stamped with a build_id — a content hash of its compiled code. It hashes the snapshot's instruction range specifically — your compiled Dart — so a change to only string constants or bundled assets leaves build_id unchanged, while a logic change produces a new one. A patch is built against a specific build_id and only applies to a binary with that exact id. This is what guarantees a patch is never applied to the wrong build, which would corrupt the app.
The engine recomputes the running binary's build_id at launch and compares it to the patch's target. A mismatch means the patch is rejected, not misapplied.
Two ways a device is matched
The safety chain
Before the engine loads any patched code, it must pass every check below. Any failure means the patch is discarded and the app runs its existing code — it never boots half-applied or corrupt.
- Signature. The patch must be Ed25519-signed by your key. Forged or tampered patches are rejected.
- build_id. The patch's target must match the running binary exactly.
- Reconstructed-target hash. For diff patches, the engine rebuilds the full new blob and verifies its hash matches what the patch declares — proving the reconstruction is byte-exact before it's trusted.
- Crash-revert. If an applied patch fails to boot cleanly, the next launch automatically falls back to the last known-good code.