MMashDiv

Updating your app to a new release

Take a new source release of the app without losing your work. Which files are yours and must be carried across, what the new zip overwrites, why your branding now survives an update, and the build number, checks and resubmission that follow.

9 min readUpdated 26 September 2026mobile, update, upgrade, release, app-config, build-number, signing, firebase

A new release of the app is a new zip: the complete source tree, not a patch and not a repository you can pull from. It has no git history. Your app, on the other hand, lives in a handful of files the zip does not contain, or contains only as template values. Updating means starting from the new tree, carrying your files across, re-applying any code changes of your own, and building with a build number the stores have not seen.

Nothing changes on your customers' phones until they install your update from the store. Your server is updated separately, on its own schedule.

Before you start

  • The project folder you built your live app from. Leave it untouched until the update is published; it is your only copy of your own changes.
  • The new release zip.
  • Your upload keystore and its passwords.
  • The highest build number you have uploaded to Google Play and to App Store Connect.
  • A list of the code changes you made yourself, if any.

Why an update is a copy, not a pull

The zip carries no .git folder, so there is nothing to git pull. If an older guide tells you to update with git pull origin main, that step does not apply to a downloaded release.

Nothing in the package compares your changes with a new release either. The tools in tool/ check your configuration and the build, not your code. Any change you made to files under lib/, android/ or ios/ has to be made again, by hand, on the new tree.

If you change the code, keep the project in a git repository of your own: commit each release exactly as it arrives, then your changes on top of it. When the next release comes, commit it the same way and let git show you what moved under your changes. Ignore assets/config/app_config.json, android/key.properties, your keystore, android/app/google-services.json and ios/Runner/GoogleService-Info.plist before the first commit. The zip ships no top-level .gitignore that would do it for you.

What is yours

These files hold your app. A new release either does not contain them, or contains a template version that must not replace yours.

File What it holds In the new zip
assets/config/app_config.json Everything that makes the build yours: baseUrl, wsBaseUrl, appName, androidApplicationId, iosBundleId, iosTeamId, the four colours, your keys Yes, but as a copy of the example, pointing at https://your-backend-url.com
assets/icons/app_icon.png The source of your launcher icons Yes, as the template's placeholder, which is not an image
android/key.properties The path and passwords of your upload key No
Your upload keystore (.jks) The key every Play update must be signed with No. Keep it outside the project folder
android/app/google-services.json Android push, if you use it No
ios/Runner/GoogleService-Info.plist iOS push, if you use it No
Your own code changes Whatever you changed under lib/, android/ or ios/ No

Three more things live inside files the new release replaces, so they are lost when you take the new file rather than because the zip lacks them:

  • The iOS Push Notifications capability. It lives in ios/Runner.xcodeproj (with the Runner.entitlements file Xcode creates). A new Xcode project does not have it. If you use push on iOS, add it again in Xcode after every update. See Build for iOS.
  • Entries you added to ios/Runner/Info.plist, for example for Google sign-in on iOS. The new Info.plist does not have them. See Sign-in methods.
  • Your version line. version: in pubspec.yaml arrives as the release's own number. See The build number.

The app_config.json inside every release is a copy of the example. A build made from it, or from a tree where the release's assets/ folder was copied over yours, talks to the placeholder host. The file exists, so the app shows no error: every screen simply fails as if your server were down. Copy your file into the new tree. The release preflight reports the placeholder as backend-url.

Why your branding survives an update now

Your app's name, both identifiers, your Apple team and your colours are all set in assets/config/app_config.json, and the icon comes from assets/icons/app_icon.png. Each reaches the build from there:

  • Android. android/app/build.gradle.kts reads appName and androidApplicationId from the config on every build.
  • iOS. The Apply app_config.json build phase writes appName into every build. dart run tool/apply_app_config.dart writes iosBundleId and iosTeamId into the Xcode project, because those two are build settings that no build phase can change.
  • Colours. The app reads them from the config when it starts.
  • Icon. dart run tool/apply_app_config.dart --icons turns your PNG into every launcher icon.

So you take the release's native files exactly as they come and carry two files of your own. Nothing of yours has to be typed into a vendor file again. The release's Xcode project carries no Apple team at all, so your team comes back either from iosTeamId in your config or from choosing it in Xcode.

Releases up to and including 5.3.9 worked differently: the name was typed into AndroidManifest.xml and Info.plist, the Android ID into build.gradle.kts, the iOS ID and team into the Xcode project, and the colours into lib/core/theme/app_themes.dart. If your live app comes from one of those and you customised any of them by hand, move each value into app_config.json before you build the update. The steps are in Branding: upgrading from an older release.

Google Play accepts an update only under the application ID it already has, and the App Store only under the bundle ID it already has. With no androidApplicationId in your config, the build uses the template's com.bicrypto.mobile, which is a different app from yours. Before you build, read the Identifiers lines that dart run tool/apply_app_config.dart prints and check both are the IDs you already publish under.

Steps

  1. Unzip the release into a new folder, next to your current project. Do not unzip it over your project, and do not copy its assets/ folder over yours.

  2. Carry your configuration across. Copy assets/config/app_config.json from your current project into the new one, replacing the release's copy. Coming from 5.3.9 or earlier with hand-edited native files? Move those values into the config first, as described above. Keys an older config carries that this release no longer reads are ignored, and you can delete them; see Keys the app ignores.

  3. Carry your icon across. Copy your assets/icons/app_icon.png into the new tree.

  4. Carry your signing across. Copy android/key.properties. Its storeFile should be an absolute path to your keystore. A relative path is resolved from the new project's android/app/, where your keystore is not.

  5. Carry your Firebase files across, if you use push: android/app/google-services.json and ios/Runner/GoogleService-Info.plist.

  6. Re-apply your own code changes, if you made any.

  7. Fetch the dependencies. From the project root:

    flutter pub get

    On a Mac, also run cd ios && pod install.

  8. Apply the config and regenerate the icons.

    dart run tool/apply_app_config.dart --icons

    This writes iosBundleId and iosTeamId, when your config sets them, into the new Xcode project, regenerates the Android and iOS launcher icons from your PNG, and prints the name, identifiers, team and colours each platform will use. Read them. Without iosTeamId, choose your team in Xcode under Signing & Capabilities instead.

  9. Restore iOS push, if you use it: in Xcode, add GoogleService-Info.plist to the Runner target and add the Push Notifications capability again. See Build for iOS.

  10. Set a new build number. See below.

  11. Check the release.

    dart run tool/apply_app_config.dart --check --release
    dart run tool/release_preflight.dart --explain

    Fix every item they report. --release makes an http:// or ws:// server address an error, because a release build refuses one on its first screen. The preflight also names native files still left from an older release, and an Xcode project with no team; if you publish on Android only, add --android-only to skip its iOS checks. The full list of its checks is in the build and tools reference.

  12. Run it once, then build and upload. A debug run against your server shows your name, your colours and live prices. Then build as usual: Build for Android, Build for iOS.

The build number

Both stores refuse a build number they have already received. The number is the part after + in the version: line of pubspec.yaml, and a new release arrives with its own line, for example version: 5.3.9+10. If your own uploads have already gone past that number, the stores refuse the update until you raise it.

Either edit the line, or leave it and pass the numbers when you build:

flutter build appbundle --release --build-name=5.4.0 --build-number=23

The version name is also what customers see in the app, under the gear icon on Home → Open-source Licenses, as for example 5.4.0 (23), and what they quote to support. If you use your own version names, keep a note of which release each one was built from.

Your server is updated separately

Updating the app does not update your server, and updating your server does not update the app on anyone's phone. Customers keep the build they have until they install yours from the store, and nothing on the server can make them. The Minimum iOS App Version and Minimum Android App Version settings in the admin are not read by the app.

Some app fixes and features need the server half as well. The Also needs column in Problems fixed in a later version says which. For example, the Market News tile on Home appears only when your core reports the news module, which it does from 6.8.2.

What it deliberately does not do

Being clear about this now saves a support ticket later.

  • It does not update over the air. The configuration is packed into each build, and there is no remote configuration. A new backend address, a new colour or a new name reaches customers only as a store update.
  • It does not merge your code. No tool in the package compares your changes with a new release. Keep your own repository if you change code.
  • It does not move your iOS bundle ID on its own. The example config ships "iosBundleId": "", which means "keep the Xcode project's ID and do not check it". Set your ID there when you want the config to be the single place it lives; then an iOS build whose project disagrees stops at the Apply app_config.json phase and says how to fix it.