@_nazmiforreal/flutter-ota 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,48 +1,73 @@
1
1
  # flutter_ota_kit
2
2
 
3
- **English** | [简体中文](README-zh.md)
4
-
5
3
  [![pub package](https://img.shields.io/pub/v/flutter_ota_kit.svg)](https://pub.dev/packages/flutter_ota_kit)
6
4
  [![Platform](https://img.shields.io/badge/platform-Android-brightgreen)](https://flutter.dev)
7
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
6
 
9
- **Open-source code push for Flutter Android.**
7
+ ### Ship Dart & asset hotfixes over the air. Your backend. No store review. No monthly bill.
10
8
 
11
- Ship Dart code and asset hotfixes over the air — no store release, no
12
- forced vendor lock-in, no per-month fees. Patches live on **your** backend
13
- (Supabase, Postgres, Cloudflare, AWS, or your own object storage) and load
14
- on the next cold start.
9
+ **flutter_ota_kit** is open-source code push for **Flutter Android**. Push a bug
10
+ fix or a new screen, and your users get it on the next cold start — no Play
11
+ Store round-trip, no vendor lock-in, no per-seat pricing. The patch lives on
12
+ **your** infrastructure (Supabase, Postgres, Cloudflare, AWS S3, PocketBase, or
13
+ your own CDN), signed and integrity-checked, with automatic crash rollback if
14
+ anything goes wrong.
15
15
 
16
16
  If you've used [Shorebird](https://shorebird.dev/),
17
17
  [CodePush](https://learn.microsoft.com/en-us/appcenter/distribution/codepush/),
18
- or [Expo EAS Update](https://docs.expo.dev/eas-update/introduction/) —
19
- flutter_ota_kit is the same OTA update model for Flutter Android, MIT-licensed,
20
- with your choice of cloud backend.
18
+ or [Expo EAS Update](https://docs.expo.dev/eas-update/introduction/), this is the
19
+ same idea for Flutter Android — MIT-licensed, self-hosted, and yours to keep.
21
20
 
22
21
  ![Feature demo: apply a patch, cold restart, and rollback](doc/feature-presentation.gif)
23
22
 
23
+ ```yaml
24
+ dependencies:
25
+ flutter_ota_kit: ^0.2.0 # one package — every backend included
26
+ ```
27
+
28
+ > **v0.2.0 is a single package.** Everything — the device SDK plus all five
29
+ > backends — now ships inside `flutter_ota_kit`. There are no more
30
+ > `flutter_ota_kit_supabase` / `_postgres` / `_cloudflare` / `_aws` /
31
+ > `_pocketbase` packages to add. One import, one dependency.
32
+
33
+ ---
34
+
35
+ ## Why teams pick it
36
+
37
+ - **You own the pipeline.** Patches sit in your Supabase project, your S3
38
+ bucket, your Cloudflare account — not someone else's cloud. No third party
39
+ sees your code or your users.
40
+ - **Zero recurring cost.** MIT-licensed. The only bill is your own storage,
41
+ which for OTA payloads is pennies.
42
+ - **Ships in one dependency.** Add `flutter_ota_kit`, run `flutter-ota init`,
43
+ deploy. Five backends are built in; you configure the one you use.
44
+ - **Safe by default.** MD5 + Ed25519 verification, automatic crash rollback,
45
+ a bad-patch blacklist, and staged rollouts — all on by default.
46
+ - **Zero-code forced-update UI.** Opt in and the SDK renders a polished
47
+ full-screen install progress screen for you. Write no UI.
48
+
24
49
  ---
25
50
 
26
51
  ## How it compares
27
52
 
28
- | | flutter_ota_kit | Shorebird | CodePush (React Native) |
29
- |-----------------------|--------------------------------------------------|-------------------------------|-------------------------------|
30
- | **Framework** | Flutter | Flutter | React Native |
31
- | **Platforms** | Android | Android + iOS | Android + iOS (retired 2025) |
32
- | **Hosting** | Your backend or your own CDN — see [Backends](doc/backends.md) | Shorebird cloud (managed) | AppCenter cloud (deprecated) |
33
- | **Update scope** | Dart AOT + Flutter assets | Dart code (engine-level diff) | JS bundle |
34
- | **Takes effect** | Next cold start | Next restart | Next restart |
35
- | **Cost** | Free (MIT) | Free tier + paid plans | — |
36
- | **Backend flexibility** | Supabase / Postgres / Cloudflare / AWS / bring-your-own | Cloud-managed only | — |
37
- | **Forced updates** | Yes — built-in progress UI | Yes | Yes |
38
- | **Crash rollback** | Automatic + bad-patch blacklist | Yes | Yes |
39
- | **Signing** | Ed25519 (Android 13+) + MD5 | Yes | — |
40
-
41
- **Choose Shorebird** if you need iOS support today or want a fully managed cloud.
42
-
43
- **Choose flutter_ota_kit** if you need OTA on infrastructure you control —
53
+ | | flutter_ota_kit | Shorebird | CodePush (React Native) |
54
+ |--------------------------|----------------------------------------------------------|-------------------------------|------------------------------|
55
+ | **Framework** | Flutter | Flutter | React Native |
56
+ | **Platforms** | Android | Android + iOS | Android + iOS (retired 2025) |
57
+ | **Hosting** | Your backend / your CDN — see [Backends](doc/backends.md) | Shorebird cloud (managed) | AppCenter cloud (deprecated) |
58
+ | **Update scope** | Dart AOT + Flutter assets | Dart code (engine-level diff) | JS bundle |
59
+ | **Takes effect** | Next cold start | Next restart | Next restart |
60
+ | **Cost** | Free (MIT) — you pay only your own storage | Free tier + paid plans | — |
61
+ | **Backend flexibility** | Supabase / Postgres / Cloudflare / AWS / PocketBase / BYO CDN | Cloud-managed only | — |
62
+ | **Forced updates** | Yes — built-in progress UI | Yes | Yes |
63
+ | **Crash rollback** | Automatic + bad-patch blacklist | Yes | Yes |
64
+ | **Signing** | Ed25519 (Android 13+) + MD5 | Yes | — |
65
+
66
+ **Choose Shorebird** if you need iOS today or want a fully managed cloud.
67
+
68
+ **Choose flutter_ota_kit** if you want OTA on infrastructure you control —
44
69
  enterprise apps, regional distribution, air-gapped deployments, or non-Play
45
- distribution channels. Bring your own backend, your own CDN, your own auth.
70
+ channels. Bring your own backend, CDN, and auth.
46
71
 
47
72
  > Google Play and some stores restrict downloading executable code at runtime.
48
73
  > flutter_ota_kit targets self-controlled, enterprise, or permissive
@@ -52,23 +77,24 @@ distribution channels. Bring your own backend, your own CDN, your own auth.
52
77
 
53
78
  ## Features
54
79
 
55
- - **OTA code push** — replace the Dart AOT `libapp.so` and Flutter assets on the
56
- next cold start, with MD5 + Ed25519 integrity checks.
57
- - **Five cloud backends** — Supabase (fully automated), Postgres, Cloudflare (D1 + R2),
58
- AWS (S3), **PocketBase** (single-binary self-hosted). Or bring your own CDN.
59
- - **Crash rollback** — automatic on boot failure with a bad-patch blacklist.
60
- Configurable via `maxCrashCount` and `verifyAfter`.
61
- - **Forced updates with built-in UI** — opt into zero-click updates with
62
- `autoApplyUpdates: true`; the SDK shows a download spinner, progress bar, and
63
- the server's OTA message during the install. No UI code from you.
64
- - **Staged rollout** — server-side cohort math (per-mille, target-cohorts) lets
80
+ - **OTA code push** — replaces the Dart AOT `libapp.so` and Flutter assets on
81
+ the next cold start, with MD5 + Ed25519 integrity checks.
82
+ - **Five backends, built in** — Supabase (fully automated), Postgres,
83
+ Cloudflare (D1 + R2), AWS (S3 + optional CloudFront), and **PocketBase**
84
+ (single-binary, self-hosted). Or point at your own CDN.
85
+ - **Automatic crash rollback** — a patch that fails to boot is rolled back and
86
+ blacklisted so it's never retried. Tunable via `maxCrashCount` /
87
+ `verifyAfter`. A runtime crash *hours* after a healthy boot no longer reverts
88
+ a good patch (bounded crash-attribution window).
89
+ - **Forced updates with a built-in UI** — set `autoApplyUpdates: true` and wrap
90
+ your app in `FlutterOtaApp`; the SDK shows a full-screen terminal-style
91
+ install screen (steps, live progress bar, speed/ETA, server message) and
92
+ cold-restarts into the new build. You write no UI.
93
+ - **Staged rollout** — server-side cohort math (per-mille + target cohorts) lets
65
94
  you ship 1% → 5% → 20% → 50% → 100% safely.
66
- - **Tooling included** — `pack` CLI (builds patches from release APKs),
67
- `flutter_ota_kit console` (web-based admin UI for the sidecar server),
68
- runtime diagnostics, sample app.
69
- - **No native plugin churn** — the patch system uses the Android
70
- `ContentProvider` to stage files; no platform channel gymnastics, no
71
- per-vendor SDKs to upgrade.
95
+ - **Batteries-included tooling** — the `flutter-ota` CLI (`init`, `build`,
96
+ `deploy`, `bundle`, `channel`, `rollback`, `migrate`, `doctor`, and a bundled
97
+ PocketBase manager), plus runtime diagnostics and a sample app.
72
98
 
73
99
  ---
74
100
 
@@ -76,44 +102,40 @@ distribution channels. Bring your own backend, your own CDN, your own auth.
76
102
 
77
103
  ```bash
78
104
  git clone https://github.com/HYPER12755/flutter-ota-kit.git
79
- cd flutter_ota_kit/example
105
+ cd flutter-ota-kit/example
80
106
  flutter pub get
81
107
  flutter build apk --release
82
108
  flutter install
83
109
  ```
84
110
 
85
- 1. Launch the app — it shows the **original** `assets/patch_demo.png`.
111
+ 1. Launch the app — it shows the **original** demo screen.
86
112
  2. Tap **Apply patch**.
87
113
  3. Swipe the app away from recents and reopen.
88
- 4. The image has changed — the asset patch took effect.
89
- 5. Tap **Rollback** → restart → original image is back.
114
+ 4. The screen has changed — the patch took effect.
115
+ 5. Tap **Rollback** → restart → the original is back.
90
116
 
91
- The example bundles a precompiled `patch.zip`. Everything runs offline on
92
- the device. For HTTP-based testing with a real backend, see
93
- [Getting Started](doc/getting-started.md) or the
94
- [Beginner Guide](doc/beginner-guide.md) (the human-narrated walkthrough).
117
+ The example bundles a precompiled patch; everything runs offline on the device.
118
+ For a full backend walkthrough, see the [Beginner Guide](doc/beginner-guide.md)
119
+ or [Getting Started](doc/getting-started.md).
95
120
 
96
121
  ---
97
122
 
98
123
  ## Requirements
99
124
 
100
- | Item | Requirement |
101
- |----------------------------|--------------------------------------------------------------------|
102
- | **Platform** | Android only (iOS / macOS / Windows / Linux / Web are no-op) |
103
- | **Dart SDK** | `>=3.13.0 <4.0.0` |
104
- | **Flutter** | `>=3.47.0` |
105
- | **Android `minSdk`** | 24 |
106
- | **Android `compileSdk`** | 36 |
107
- | **ABI** | `armeabi-v7a` / `arm64-v8a` / `x86_64` |
108
- | **NDK** | 27.0.12077973+ |
109
- | **AGP** | 8.11.1+ (including AGP 9.x) |
110
- | **Kotlin** | 2.2.20+ (or AGP 9 built-in Kotlin) |
111
- | **Java / JVM** | 17 |
112
-
113
- On iOS / macOS / Windows / Linux / Web, every API is safe to call but is a
114
- **no-op** — the plugin logs a one-time warning and returns safe defaults.
115
- This makes it safe to ship `flutter_ota_kit` in cross-platform code without
116
- guarding every call site.
125
+ | Item | Requirement |
126
+ |--------------------------|----------------------------------------------------------|
127
+ | **Platform** | Android only (iOS / macOS / Windows / Linux / Web are no-ops) |
128
+ | **Dart SDK** | `>=3.13.2 <4.0.0` |
129
+ | **Flutter** | `>=3.47.2` |
130
+ | **Android `minSdk`** | 24 |
131
+ | **Android `compileSdk`** | 36 |
132
+ | **ABI** | `armeabi-v7a` / `arm64-v8a` / `x86_64` |
133
+ | **NDK** | 27.0.12077973+ |
134
+ | **Java / JVM** | 17 |
135
+
136
+ On non-Android platforms every API is safe to call but is a **no-op** — the
137
+ plugin logs a one-time warning and returns safe defaults, so you can ship
138
+ `flutter_ota_kit` in cross-platform code without guarding every call site.
117
139
 
118
140
  ---
119
141
 
@@ -122,130 +144,119 @@ guarding every call site.
122
144
  ### 1. Install
123
145
 
124
146
  ```yaml
125
- # pubspec.yaml
126
147
  dependencies:
127
- flutter_ota_kit: ^0.1.10
148
+ flutter_ota_kit: ^0.2.0
149
+ ```
150
+
151
+ ### 2. Scaffold your backend (CLI)
152
+
153
+ ```bash
154
+ # installs the flutter-ota CLI globally
155
+ npm i -g @_nazmiforreal/flutter-ota
156
+
157
+ # scaffold config + a generated setup file for your backend
158
+ flutter-ota init supabase # or: postgres | cloudflare | aws | pocketbase
128
159
  ```
129
160
 
130
- ### 2. Initialize
161
+ `init` writes a `.flutter_ota_kit/config.json`, an `.env` template for secrets,
162
+ and a `lib/flutter_ota_kit_setup.dart` helper you call from `main()`.
131
163
 
132
- Call `setupFlutterOta()` (or the bare `FlutterPatcher.init()`) before `runApp()`:
164
+ ### 3. Initialize in your app
165
+
166
+ Call the generated `setupFlutterOta()` (or wire it by hand) before `runApp`:
133
167
 
134
168
  ```dart
135
169
  import 'package:flutter/material.dart';
136
170
  import 'package:flutter_ota_kit/flutter_ota_kit.dart';
137
- import 'flutter_ota_kit_setup.dart'; // generated by `flutter-ota init <backend>`
171
+ import 'flutter_ota_kit_setup.dart'; // generated by `flutter-ota init`
138
172
 
139
173
  Future<void> main() async {
140
174
  WidgetsFlutterBinding.ensureInitialized();
141
- await setupFlutterOta(); // configures the backend + zero-click forced updates
175
+ await setupFlutterOta(); // configures the backend + forced updates
142
176
  runApp(const FlutterOtaApp(child: MyApp()));
143
177
  }
144
178
  ```
145
179
 
146
- `FlutterOtaApp` is the zero-code wrapper that gives the SDK an
147
- `Overlay` to render the built-in forced-update progress UI into. The user
148
- sees a download spinner + progress bar + the server's OTA message during
149
- the install — no UI from you.
150
-
151
- Disable the overlay app-wide with `FlutterPatcher.showUpdateUi = false`
152
- (or `FlutterOtaApp(showUpdateUi: false)`), or host it yourself by
153
- assigning `MaterialApp.navigatorKey = FlutterPatcher.navigatorKey`.
180
+ `FlutterOtaApp` is the zero-code wrapper that lets the SDK render its built-in
181
+ forced-update screen. Disable it app-wide with
182
+ `FlutterPatcher.showUpdateUi = false` (or `FlutterOtaApp(showUpdateUi: false)`),
183
+ or host it yourself by setting `MaterialApp.navigatorKey =
184
+ FlutterPatcher.navigatorKey`.
154
185
 
155
- ### 3. Build a patch
186
+ Prefer wiring the backend directly? Every backend has a `configureX` helper:
156
187
 
157
- Rebuild the release APK, then run `pack` against the new APK to produce
158
- `dist/patch.zip` and `dist/manifest.json`:
159
-
160
- ```bash
161
- dart run flutter_ota_kit:pack \
162
- --apk build/app/outputs/flutter-apk/app-release.apk \
163
- --version 1.0.0-h1 \
164
- --target-version-code 100
188
+ ```dart
189
+ FlutterPatcher.configureSupabase(SupabaseUpdateConfig(
190
+ supabaseUrl: 'https://<ref>.supabase.co',
191
+ anonKey: '<anon-key>', // public, RLS-protected reads
192
+ bucket: 'bundles',
193
+ channel: 'production',
194
+ platform: Platform.android,
195
+ updateStrategy: UpdateStrategy.appVersion,
196
+ appVersion: '1.0.0', // or omit — auto-detected from versionName
197
+ ));
198
+ await FlutterPatcher.init(autoApplyUpdates: true);
165
199
  ```
166
200
 
167
- To include assets (since 0.1.3), append `--assets`:
201
+ ### 4. Build a patch
202
+
203
+ Rebuild the release APK, then pack it into a device-ready `patch.zip`:
168
204
 
169
205
  ```bash
170
- dart run flutter_ota_kit:pack \
206
+ flutter build apk --release
207
+ flutter-ota build \
171
208
  --apk build/app/outputs/flutter-apk/app-release.apk \
172
209
  --version 1.0.1 \
173
- --target-version-code 100 \
174
- --assets assets/hero.png,assets/strings/zh.json
210
+ --target-version-code 100
175
211
  ```
176
212
 
177
- Each asset must be registered in the new APK's `pubspec.yaml`. Upload
178
- `patch.zip` to your backend (or CDN) and have your update endpoint return
179
- a `PatchInfo` pointing at it.
213
+ Every ABI in the APK is included, so one bundle serves all devices. Add
214
+ `--assets assets/hero.png,assets/strings/zh.json` to overlay updated assets.
180
215
 
181
- ### 4. Apply a patch (most apps just do this)
216
+ ### 5. Deploy
182
217
 
183
- ```dart
184
- final result = await FlutterPatcher.applyPatch(
185
- PatchInfo(
186
- version: 'fix-1',
187
- patchUrl: 'https://your-cdn.com/v100/patch.zip',
188
- md5: '0123456789abcdef0123456789abcdef',
189
- targetVersionCode: 100,
190
- ),
191
- );
192
-
193
- if (result.ok) {
194
- // Patch takes effect on the next cold start.
195
- }
218
+ ```bash
219
+ flutter-ota deploy \
220
+ --source dist \
221
+ --channel production \
222
+ --target-app-version 1.0.0 \
223
+ --force
196
224
  ```
197
225
 
198
- If you manage the download yourself, use `applyPatchBytes`:
226
+ That's it — devices pick up the patch on their next check.
199
227
 
200
- ```dart
201
- final bytes = await loadPatchFromYourSource();
202
- final result = await FlutterPatcher.applyPatchBytes(
203
- bytes,
204
- version: '1.0.0-h1',
205
- targetVersionCode: 100,
206
- );
207
- ```
228
+ ### 6. Roll back (server side)
208
229
 
209
- ### 5. Roll back
210
-
211
- ```dart
212
- await FlutterPatcher.rollback();
230
+ ```bash
231
+ flutter-ota rollback --channel production
213
232
  ```
214
233
 
215
- Deletes the current patch. The app reverts to the APK's built-in version on
216
- the next cold start. This is **not** the same as a server-side rollback —
217
- this deletes the local patch, it doesn't change what's in your backend.
234
+ Or on the device, `FlutterPatcher.rollback()` deletes the local patch and
235
+ reverts to the APK's built-in version on the next cold start.
218
236
 
219
237
  ---
220
238
 
221
239
  ## How it works
222
240
 
223
241
  ```text
224
- ┌─────────────────┐
225
- │ Your backend │ Supabase / Postgres / Cloudflare / AWS / PocketBase / your CDN
226
- │ (or CDN) │ stores: patch.zip (the diff) + manifest.json (the metadata)
242
+ ┌─────────────────┐ Supabase / Postgres / Cloudflare / AWS / PocketBase / CDN
243
+ │ Your backend │ stores: patch.zip (the diff) + manifest.json (metadata)
227
244
  └────────▲────────┘
228
245
  │ 1. check-for-update on launch
229
- │ 2. server returns PatchInfo (url, md5, signature, version, shouldForceUpdate)
230
- │
246
+ │ 2. backend returns PatchInfo (url, md5, signature, force flag)
231
247
  ┌────────┴────────┐
232
- │ The app │
233
- │ │ 3. download patch.zip
248
+ │ The app │ 3. download patch.zip
234
249
  │ │ 4. verify MD5 + Ed25519 signature
235
250
  │ │ 5. write to local patch dir (atomic rename)
236
251
  │ │ 6. cold-restart (forced) OR next cold start (staged)
237
252
  │ │ 7. loader hook reads patched libapp.so + asset overlays
238
- │ │ 8. boot succeeds → keep patch
239
- │ │ boot fails → auto-rollback + blacklist
253
+ │ │ 8. boot ok → keep · boot fails → auto-rollback + blacklist
240
254
  └─────────────────┘
241
255
  ```
242
256
 
243
- A patch is a **byte-for-byte diff** of the new APK's `libapp.so` against the
244
- old one (the versionCode the user has installed). Asset patches overlay
245
- `flutter_assets/` files by path. The patch never ships inside the running
246
- process — it loads on the next cold start. The cold-restart for forced
247
- updates goes through `restart_app` (with `RestartMode.process`, which
248
- actually exits the process so the new code reloads).
257
+ A patch is a byte-for-byte diff of the new APK's `libapp.so` against the
258
+ version the user has installed; asset patches overlay `flutter_assets/` by path.
259
+ Nothing is swapped into the running process — it loads on the next cold start.
249
260
 
250
261
  Full internals: [Architecture](doc/architecture.md).
251
262
 
@@ -253,139 +264,68 @@ Full internals: [Architecture](doc/architecture.md).
253
264
 
254
265
  ## What can and can't be patched
255
266
 
256
- | ✅ Hot-patchable | ❌ Not hot-patchable |
257
- |-----------------------------------------------------------------|-----------------------------------------------------------------------|
258
- | Anything in `lib/` — widgets, logic, routes, constants, tests | Native code (Kotlin / Java / C++ in `android/src/main/`) |
259
- | Pure-Dart package upgrades (same native side) | `AndroidManifest.xml` changes |
260
- | Flutter asset files (registered in `pubspec.yaml`) | APK `res/` (icons, layouts, strings.xml) |
261
- | New `Image.asset()` / `rootBundle.load()` calls pick up new bytes | Flutter Engine upgrades |
262
- | | Adding or removing native plugins |
263
- | | Removing assets that exist in the base APK |
264
- | | `pubspec.yaml` font registration changes |
267
+ | ✅ Hot-patchable | ❌ Not hot-patchable |
268
+ |------------------------------------------------------|----------------------------------------------------------|
269
+ | Anything in `lib/` — widgets, logic, routes, constants | Native code (Kotlin / Java / C++ in `android/src/main/`) |
270
+ | Pure-Dart package upgrades (same native side) | `AndroidManifest.xml` changes |
271
+ | Flutter asset files registered in `pubspec.yaml` | APK `res/` (icons, layouts, strings.xml) |
272
+ | New `Image.asset()` / `rootBundle.load()` calls | Flutter Engine upgrades |
273
+ | | Adding or removing native plugins |
274
+ | | Removing assets that exist in the base APK |
265
275
 
266
- For edge cases (ProGuard/R8, multi-ABI / flavors, state migrations, asset
267
- overlay quirks), see [API Reference → What can be patched](doc/api-reference.md#what-can-and-cannot-be-patched).
276
+ See [API Reference → What can be patched](doc/api-reference.md#what-can-and-cannot-be-patched)
277
+ for edge cases (ProGuard/R8, multi-ABI/flavors, state migrations).
268
278
 
269
279
  ---
270
280
 
271
281
  ## Safety
272
282
 
273
- ### Crash protection (on by default)
274
-
275
- The plugin is **fail-fast**: if a patch causes a boot failure, it auto-rolls
276
- back and blacklists the offending version so it won't be retried. Tunable
277
- via `maxCrashCount` (default 1) and `verifyAfter` (default 5s).
278
-
279
- Design and Android version differences:
280
- [Crash Protection](doc/crash-protection.md).
281
-
282
- ### Integrity & signing
283
-
284
- - **MD5** verification is strongly recommended; omit only for quick testing.
285
- - **Ed25519 signature** verification is available on Android 13+ (API 33).
286
- Falls back to MD5-only on older Android (with a warning).
287
- - Patches are bound to the host APK's `versionCode` — old patches expire
288
- after an APK upgrade and the server stops serving them.
289
- - Always download over HTTPS. Keep private signing keys on the server only,
290
- never in the app bundle.
291
-
292
- ### Production tips
293
-
294
- - **Stage your rollout** (1% → 5% → 20% → 50% → 100%) and monitor crash
295
- rate at each stage. The SDK records `lastBootDiagnostic` per device.
296
- - **Report diagnostics** to your analytics pipeline so you can spot a bad
297
- patch before users hit the app.
298
- - **Prepare for emergency rollback** — stop returning the bad patch from
299
- your endpoint; devices that already tripped crash protection have
300
- rolled back locally and won't re-download.
301
-
302
- Full release workflow + diagnostic-reporting code:
303
- [Production Playbook](doc/production-playbook.md).
304
-
305
- ---
306
-
307
- ## FAQ
308
-
309
- **Q: Must the patch and base APK use the same Flutter version?**
310
- A: Yes. `libapp.so` is tightly coupled to the Flutter Engine ABI.
311
- After upgrading the SDK, ship a new release.
312
-
313
- **Q: Why doesn't a patch take effect immediately?**
314
- A: Once `libapp.so` is loaded by the current process, it can't be swapped
315
- at runtime. The patch is written to disk and loaded on the next cold
316
- start. Forced updates trigger a process restart so the new code loads
317
- immediately.
318
-
319
- **Q: Why does each patch need a `targetVersionCode`?**
320
- A: Two reasons: (1) old patches expire after an APK upgrade (so a user on
321
- v2 doesn't accidentally load a v1-targeted patch), and (2) the server
322
- won't ship a patch built for v1 to a user on v2 (the SDK filters them
323
- out at update-check time).
324
-
325
- **Q: How does the client report its app version, and why must it match `--target-app-version`?**
326
- A: The client reports an `appVersion` that the backend matches against
327
- each bundle's `target_app_version`. By default this is **auto-detected at
328
- runtime** from the host app's `versionName` via `package_info_plus` — no
329
- build flag needed. You can override it explicitly with
330
- `--dart-define=APP_VERSION=1.2.3` or `SupabaseUpdateConfig.appVersion`.
331
- The backend keeps only bundles whose `target_app_version` is
332
- semver-compatible with the reported version. **If they don't match, the
333
- backend returns no bundle and the app silently stays "up to date"** — so
334
- always deploy with `--target-app-version` equal to the app's real
335
- `versionName` (e.g. `1.0.1` for a `version: 1.0.1+2` pubspec). A mismatched
336
- version is the most common cause of "the update never arrives".
337
-
338
- More questions: [Full FAQ](doc/faq.md).
283
+ - **Crash protection (on by default).** If a patch fails to boot, the SDK
284
+ auto-rolls back and blacklists the offending version. Tune with
285
+ `maxCrashCount` (default 1) and `verifyAfter` (default 5s). Crash attribution
286
+ is bounded to a boot window, so an unrelated crash long after a healthy boot
287
+ won't revert a working patch. See [Crash Protection](doc/crash-protection.md).
288
+ - **Integrity & signing.** MD5 is strongly recommended; Ed25519 signature
289
+ verification is available on Android 13+ (falls back to MD5-only below, with a
290
+ warning). Patches are bound to the host APK's `versionCode`, so stale patches
291
+ expire after an app upgrade. Keep private signing keys on your server, never
292
+ in the app bundle.
293
+ - **Staged rollout.** Ship 1% → 5% → 20% → 50% → 100% and watch crash rate at
294
+ each stage. The SDK records `lastBootDiagnostic` per device.
295
+
296
+ Full release workflow: [Production Playbook](doc/production-playbook.md).
339
297
 
340
298
  ---
341
299
 
342
300
  ## Documentation
343
301
 
344
- **Guides** (narrative, in reading order)
302
+ **Guides**
345
303
 
346
- - [Beginner Guide](doc/beginner-guide.md) — zero-to-first-OTA walkthrough, as a human would do it
347
- - [Getting Started](doc/getting-started.md) — scaffold → build → deploy in 5 minutes
348
- - [Developer Guide](doc/developer-guide.md) — full workflow reference (init, migrate, build, deploy, SDK API, targeting, troubleshooting)
349
- - [Configuration](doc/configuration.md) — every env var, `.env`, resolution order, secrets policy
350
- - [Backends](doc/backends.md) — Supabase / Postgres / Cloudflare / AWS / PocketBase setup, env vars, CLI snippets
351
- - [Production Playbook](doc/production-playbook.md) — staged rollout, diagnostics, emergency rollback
304
+ - [Beginner Guide](doc/beginner-guide.md) — zero to first OTA, narrated
305
+ - [Getting Started](doc/getting-started.md) — scaffold → build → deploy
306
+ - [Developer Guide](doc/developer-guide.md) — full workflow reference
307
+ - [Configuration](doc/configuration.md) — env vars, `.env`, resolution order
308
+ - [Backends](doc/backends.md) — Supabase / Postgres / Cloudflare / AWS / PocketBase setup
309
+ - [Production Playbook](doc/production-playbook.md) — staged rollout, diagnostics, rollback
352
310
 
353
311
  **Reference**
354
312
 
355
- - [API Reference](doc/api-reference.md) — `FlutterPatcher` methods, error codes, asset patching
356
- - [Architecture](doc/architecture.md) — internals, server protocol, signing, advanced config
357
- - [CLI Reference](doc/cli-reference.md) — every command, subcommand, and flag
358
- - [Crash Protection](doc/crash-protection.md) — auto-rollback, blacklist, Android version differences
359
- - [Golden Testing](doc/golden-testing.md) — how Flutter's pixel-perfect test system works (and how this project uses it)
360
-
361
- **Other**
362
-
313
+ - [API Reference](doc/api-reference.md) — `FlutterPatcher`, configs, overlay, error codes
314
+ - [Architecture](doc/architecture.md) — internals, protocol, signing
315
+ - [CLI Reference](doc/cli-reference.md) — every command and flag
316
+ - [Crash Protection](doc/crash-protection.md) — rollback, blacklist, Android differences
363
317
  - [FAQ](doc/faq.md) — versioning, cold start, store policy
364
- - [CHANGELOG.md](CHANGELOG.md) — release notes
365
-
366
- 中文文档: [README-zh.md](README-zh.md) · [api-reference-zh](doc/api-reference-zh.md) ·
367
- [architecture-zh](doc/architecture-zh.md) · [crash-protection-zh](doc/crash-protection-zh.md) ·
368
- [getting-started-zh](doc/getting-started-zh.md) · [production-playbook-zh](doc/production-playbook-zh.md) ·
369
- [faq-zh](doc/faq-zh.md)
370
-
371
- ---
372
-
373
- ## Who's using it?
374
-
375
- If you run flutter_ota_kit in production, [open an issue](https://github.com/HYPER12755/flutter-ota-kit/issues) and tell us about your use case — we'd love to list you here.
376
318
 
377
319
  ---
378
320
 
379
321
  ## Contributing
380
322
 
381
- Issues and PRs are welcome.
382
-
383
- Before submitting, please make sure:
323
+ Issues and PRs welcome. Before submitting:
384
324
 
385
- - `flutter analyze` reports no warnings
325
+ - `flutter analyze` reports no issues
386
326
  - `flutter test` is fully green
387
- - If you touched native code, you've run a real-device end-to-end patch / rollback flow
388
- - If you added a new public API, you've documented it on `dartdoc_options.yaml` topics (the doc appears on pub.dev)
327
+ - If you touched native code, run a real-device patch / rollback end-to-end
328
+ - Document new public APIs (they surface on pub.dev)
389
329
 
390
330
  ---
391
331
 
@@ -613,14 +613,22 @@ internal class PatchManager(
613
613
  }
614
614
 
615
615
  private fun extractBaseAssetsIfNeeded(): Int {
616
- // Extract base APK assets to assets/0/ if not already done
616
+ // Store the base APK's flutter_assets as a single archive for rollback.
617
+ // We keep ONLY the compressed archive (not an uncompressed mirror dir):
618
+ // the previous implementation left both `assets/0/` (uncompressed) AND
619
+ // `assets/0/flutter_assets.apk` (zip) on disk — two full copies of every
620
+ // bundled asset. The uncompressed copy is never read (rollback re-extracts
621
+ // from the archive), so it was pure waste.
617
622
  if (!baseAssetsArchive.exists()) {
618
623
  baseAssetsArchive.parentFile?.mkdirs()
624
+ val tmp = File(assetsHistoryDir, "0.tmp")
619
625
  try {
620
- copyInstalledFlutterAssets(File(assetsHistoryDir, "0"))
621
- writeFlutterAssetsArchive(File(assetsHistoryDir, "0"), baseAssetsArchive)
626
+ copyInstalledFlutterAssets(tmp)
627
+ writeFlutterAssetsArchive(tmp, baseAssetsArchive)
622
628
  } catch (e: Exception) {
623
629
  Log.w(TAG, "failed to extract base assets", e)
630
+ } finally {
631
+ tmp.deleteRecursively()
624
632
  }
625
633
  }
626
634
  return 0
@@ -998,8 +1006,19 @@ internal class PatchManager(
998
1006
  meta.put("assetsRef", assetsRef)
999
1007
  archiveCurrentPatchToHistory(meta)
1000
1008
  } else {
1001
- // First patch ever - extract base assets
1002
- assetsRef = extractBaseAssetsIfNeeded()
1009
+ // First patch ever.
1010
+ //
1011
+ // Only extract the base APK's flutter_assets (as a rollback
1012
+ // reference) when this patch actually ships assets. A Dart-only
1013
+ // (code-only) patch never overlays assets, so extracting the
1014
+ // whole base asset tree here is pure waste — it was the main
1015
+ // cause of a first code-patch ballooning app storage by 100s of
1016
+ // MB. Code-only patches load assets straight from the APK.
1017
+ assetsRef = if (finalAssets != null) {
1018
+ extractBaseAssetsIfNeeded()
1019
+ } else {
1020
+ 0
1021
+ }
1003
1022
  meta.put("assetsRef", assetsRef)
1004
1023
  }
1005
1024