Skip to content

White Labeling

White labeling means publishing the same app to the stores many times, once per customer, each with its own name, icon, and bundle ID. With Shorebird, you fix a bug in the shared Dart code once and one CI job patches every brand’s app. Your agreements with the app stores still apply; see Store compliance.

The setup is:

  1. Give each store app its own Shorebird app.
  2. Create a service account for CI.
  3. Run shorebird init once in each app.
  4. Release each app with shorebird release.
  5. Patch every app from one CI job, trying each fix on a few apps first.

Each app in the stores needs its own Shorebird app, with its own app ID. One Shorebird app covers both Android and iOS for that store app, so a fleet of 100 brands on both stores is 100 Shorebird apps.

Two brands can’t share a Shorebird app because a patch belongs to one release. Shorebird matches patches to devices by app ID and release version, and a release holds one build per platform. A patch is built against that build and installs only on it. Separate apps also give each brand its own releases, tracks, rollbacks, and insights, so you can roll back a patch for one brand without touching the others.

How you get one app per brand depends on how your project is laid out:

  • One Flutter project per brand (for example, host apps generated from a template that share the Dart code as a package): run shorebird init in each project. Each gets its own shorebird.yaml.
  • One Flutter project with a flavor per brand: shorebird init creates one Shorebird app per flavor and lists them under flavors: in shorebird.yaml. Pass --flavor to shorebird release and shorebird patch. See Apps with flavors.

Give your pipeline a service account rather than a person’s key, so it keeps working when people change roles or leave. Pick its role by what the pipeline does:

  • App Manager if CI runs shorebird init, which creates apps.
  • Developer if CI only creates releases and patches.

Create a key for the account and store it in your CI’s secrets as SHOREBIRD_TOKEN. The Shorebird CLI reads it from that environment variable. See Use a key from CI or a script.

Run shorebird init in each brand’s project, for example as a step of the template that generates a new brand, then commit the shorebird.yaml it creates and the pubspec.yaml change:

cd apps/brand_a
shorebird init --org-id=<org id> --display-name="Brand A"

In CI, shorebird init doesn’t ask questions. Without --display-name it names the app after the name in pubspec.yaml, which is often the same for every brand, and without --org-id it fails if the account can create apps in more than one organization. See Non-interactive mode.

Run it once per app. If shorebird.yaml already exists, shorebird init stops. Don’t pass --force to an app you’ve already released: it creates a new Shorebird app with a new app ID, and builds already in the stores won’t receive patches published to it.

With flavors, add a brand by adding its flavor and running shorebird init again without --force. It creates apps for the new flavors and keeps the existing ones. It names each new app after the existing app with the flavor in parentheses, and ignores --display-name, so rename it in the console if you need to. It also rewrites shorebird.yaml with only app_id and flavors, so check the diff and put back any other settings, such as auto_update: false, before you commit.

Every build you upload to the stores must be the one shorebird release made. Release each brand’s app for both platforms in one command:

cd apps/brand_a
shorebird release --platforms=android,ios

With flavors, run it once per flavor with --flavor=brand_a. Releasing and patching for iOS need a Mac. Upload the builds the command produces; a build of the same version made any other way can’t install patches (see Patches can’t install because the app isn’t the build Shorebird released). See Releasing for the details of each platform.

Once each app has a release in the stores, a fix to the shared Dart code is a loop over the apps. This script patches each one and lists any that failed at the end, rather than stopping at the first failure:

#!/usr/bin/env bash
# Needs SHOREBIRD_TOKEN set to the service account's key.
failed=()
for app in apps/*/; do
  if ! (cd "$app" && shorebird patch --platforms=android,ios \
      --release-version=latest --track=stable); then
    failed+=("$app")
  fi
done
if [ ${#failed[@]} -gt 0 ]; then
  echo "Patching failed for: ${failed[*]}"
  exit 1
fi

With flavors, loop over the flavor names instead and pass --flavor "$flavor" from the project’s root. The iOS patches need a macOS runner; on a Linux runner, pass --platforms=android and patch iOS from a macOS job.

--release-version=latest patches each app’s most recently updated release for each platform, so Android and iOS can land on different versions. If a brand has more than one release in use, patch each version with --release-version=<version>. Each patch is a build, so a large fleet takes a while; split the apps across parallel CI jobs to shorten it.

If a patch changes native code or assets, shorebird patch stops in CI. See Patch stops in CI with Not publishing.

Before a fix reaches the whole fleet, try it on your flagship app and two or three other brands:

  1. Run the loop over those apps with --track=staging. Devices check the stable track by default, so users don’t get these patches.
  2. Test each patch with shorebird preview --track=staging, or on a device subscribed to the staging track.
  3. Promote the patches to stable in the console, or with shorebird patches promote --release-version=<version> --patch-number=<n> in each app’s directory (with flavors, add --flavor=<brand>). Android and iOS each get their own patch, with its own number and possibly a different release version, so promote each platform’s patch. shorebird patch --json prints the release_version and patch_number of each. See Promoting a patch between tracks.
  4. Run the loop over the remaining apps with --track=stable.

See Staging Patches for a walkthrough of the staging track.