white_label_kit
The modern, automated flavor & multi-tenant white-label toolkit for Flutter.
Easily manage multiple branded apps, flavors, and client tenants from a single Flutter codebase. Define all your tenants in white_label.yaml, and let white_label_kit automate Android Gradle flavors, iOS Xcode build schemes, IDE configurations, and compile-time asset isolation.
๐ก Why white_label_kit?
Managing multiple flavors or white-label client apps in Flutter usually means:
- Hand-editing complex
android/app/build.gradle.ktsproduct flavors. - Manually creating and wiring iOS Xcode build configurations, schemes, and bundle identifiers.
- Risking asset leakage where one tenant's logos or credentials accidentally get bundled into another tenant's app.
- Manually configuring IDE debug and build tasks for every new flavor.
white_label_kit automates all of this:
- ๐ Single Source of Truth: Declare all tenants, bundle IDs, colors, API endpoints, environments, and feature flags in one
white_label.yaml. - ๐ค Native File Automation: Patches Android Gradle and iOS Xcode schemes automatically with
dart run white_label_kit:configure. - ๐จ Icon & Splash Automation: Opt-in per-tenant launcher icon and native splash generation โ including the iOS Xcode wiring step most guides forget (see ยง5).
- ๐ Per-Environment Runtime Config: The same tenant/brand can target
staging/production/anything else via--env, without a second tenant (see ยง6). - ๐ก๏ธ Asset Isolation: Guarantees only the active tenant's assets and configs are compiled into the binary โ never every tenant's data baked into every build.
- โก Interactive CLI Runner: Launch
dart run white_label_kitto easily run or build APK, AAB, and iOS apps without memorizing long commands. - ๐ป 1-Click IDE Configurations: Generates ready-to-use Run/Build configurations for Android Studio, IntelliJ, and VS Code.
- ๐ Type-Safe Runtime API: Access tenant metadata cleanly in your Flutter widgets and services using
WhiteLabelRuntime. - ๐ฉบ
doctor: One command that flags missing assets, stale generated files, and missing peer dependencies (flutter_native_splash) before a build fails on them.
๐ Getting Started
1. Add Dependency
Add white_label_kit to your Flutter project's dependencies โ not
dev_dependencies:
flutter pub add white_label_kit
Or manually in pubspec.yaml:
dependencies:
white_label_kit: ^0.0.5
Why a regular dependency, not dev-only:
lib/white_label.g.dart(the generated file โ see step 3) importsWhiteLabelRuntime/WhiteLabelThemefrom this package and is compiled directly into your app, read by your own runtime code (whiteLabelRuntime.environment.apiBaseUrl, theme colors, feature flags, etc.). It is not purely a build-time codegen tool the waybuild_runneris โ putting it underdev_dependencieswould still happen to compile for a leaf app, but is the wrong semantic declaration for a package your shipped binary actually reads from at runtime, and would break if this package's code ever needed to reach another package that's only resolved viadependencies.
2. Initialize Configuration
Generate a starter white_label.yaml in your project root:
dart run white_label_kit:init
3. Add Your Tenants / Brands
Add a new brand with a single command:
dart run white_label_kit:add-tenant acme "Acme App" com.example.acme --logo path/to/acme_logo.png
This automatically creates the configuration entry in white_label.yaml
and the asset folder tenants/acme/, copying your real logo in if
--logo was given. Omit --logo and it writes a placeholder
tenants/acme/logo.png instead โ replace that file with the real logo
before shipping (this is the one step white_label_kit genuinely can't
automate for you: it doesn't know what your brand's logo looks like).
4. Configure Android & iOS Native Files
Sync all native Gradle flavors, Xcode schemes, and IDE run configurations:
dart run white_label_kit:configure
Run dart run white_label_kit:doctor any time to sanity-check the current
setup โ missing assets, a stale lib/white_label.g.dart, a missing
flutter_native_splash dependency if splash_generate is on, etc.
5. Launcher Icons & Native Splash (per tenant)
Launcher/notification icons and the native splash screen are generated by
two well-established, purpose-built packages โ
icons_launcher and
flutter_native_splash
โ not re-modeled by white_label_kit itself.
icons_launcher is a real dependency of this package, so dart run icons_launcher:create resolves for your app with nothing added to your
own pubspec.yaml. flutter_native_splash can't be a dependency of
this package the same way (it needs the Flutter SDK to resolve, which
this package deliberately doesn't โ see maybeGenerateNativeSplash's doc
comment for the full reasoning). Add flutter_native_splash to your
own app's pubspec.yaml (flutter pub add flutter_native_splash) if
you want the splash generation below โ icon generation needs no such
step.
Opt-in auto-generation (recommended default): declare
features: { icon_generate: true } / { splash_generate: true } for a
tenant in white_label.yaml, and configure/build create
icons_launcher-<id>.yaml / flutter_native_splash-<id>.yaml for you โ
derived from that tenant's assets.icon/assets.logo (icon) or
assets.splash/assets.icon/assets.logo + theme.primary_color (splash)
โ only if the file doesn't already exist. Nothing to hand-author for
the common case, and a file you've already customized is never touched or
overwritten:
tenants:
acme:
features:
icon_generate: true
splash_generate: true
Both flags are off by default โ a tenant that declares neither sees no change in behavior at all.
The auto-created icons_launcher-<id>.yaml includes an adaptive icon
(Android 8.0+/API 26) by default โ adaptive_foreground_image reuses the
tenant's icon/logo, adaptive_background_color uses theme.primary_color
(white if unset). A reasonable automatic default, not a substitute for a
properly-padded, transparent foreground asset โ hand-author the file
with a dedicated foreground image for a polished result.
iOS storyboard registration is automatic โ for the opt-in flag only.
flutter_native_splash:create only writes
ios/Runner/Base.lproj/LaunchScreen<Tenant>.storyboard to disk โ Xcode
never bundles a resource it doesn't know about, so a stock run of that
command alone silently produces a splash screen that never ships. When
splash_generate: true triggers a successful flutter_native_splash:create
run, white_label_kit registers that storyboard into Runner.xcodeproj's
Resources build phase for you right after (idempotent โ safe to re-run,
best-effort โ a missing ruby/xcodeproj gem is reported as a warning in
the output, never a crash).
Manual (full control) โ skip the flags: hand-author
icons_launcher-acme.yaml / flutter_native_splash-acme.yaml yourself
using either package's full config reference (adaptive icon
background/foreground, dark-mode variants, fullscreen, per-platform
overrides, and everything else either supports), then run:
dart run icons_launcher:create --flavor acme
dart run flutter_native_splash:create --flavor acme
Going this route puts you outside the automatic registration above โ
you must still register the storyboard into Xcode yourself
(ios/Runner.xcodeproj's Resources build phase) before it will actually
appear in the built app.
6. Staging / Production (--env)
environments: + --env switches runtime config (API URL and
whatever else you put in custom:) for the SAME tenant/brand โ it is
not a second tenant. Icon, theme, bundle id, and app name all stay
whatever the tenant already declares; only environment changes. See
Configuration File below for the
YAML shape.
staging/production below are just this README's example names โ the
key under environments: is an arbitrary string you choose, not a
fixed/reserved keyword. Name it whatever matches your own release
process (qa, uat, demo, beta, ...) and pass that exact name to
--env.
dart run white_label_kit:generate --tenant acme --env staging
dart run white_label_kit:configure --tenant acme --env staging
dart run white_label_kit:build --tenant acme --env staging --platform android
dart run white_label_kit:run --tenant acme --env staging
Omit --env anywhere above and the tenant's default environment: block
is used โ fully backward compatible, a white_label.yaml that never
declares environments: needs no change. Passing an --env name the
tenant never declared is a hard error (never a silent fallback to the
default) โ you can't accidentally ship "staging" with production's API
URL baked in.
build/run/configure all (re)generate lib/white_label.g.dart for
whichever tenant/environment they actually resolved to, every time they
run โ there's no separate "don't forget to regenerate" step to remember.
๐ฅ๏ธ Running & Building Your App
Option A: Interactive Terminal Menu (Recommended)
Launch the interactive runner:
dart run white_label_kit
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โจ WHITE_LABEL_KIT RUNNER & BUILDER โ
โ Automated Multi-Tenant Flutter CLI & Launcher โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ SELECT TENANT:
[0] Acme App [acme] (Default)
Enter tenant number (default: acme): 0
โก SELECT ACTION:
[1] โถ๏ธ Run in Debug Mode (Simulator / Connected Device)
[2] โก Run in Release Mode (Device)
[3] ๐ Build Release APK (Android)
[4] ๐ฆ Build Release AppBundle / AAB (Google Play Store)
[5] ๐ Build Release iOS (Simulator / Archive)
[6] ๐ง Configure All Tenants (white_label_kit:configure)
[7] โ Add New Tenant (white_label_kit:add-tenant)
[8] โ Remove Tenant (white_label_kit:remove-tenant)
[9] ๐ Analyze & Health Check (Flutter Analyze + Tests)
[0] ๐ช Exit
Option B: Flutter CLI Commands
You can also run or build directly with standard Flutter commands โ this
bypasses white_label_kit's own build/run (so it does not
regenerate lib/white_label.g.dart for you; run generate/configure
first if you switched tenant or --env):
# Run tenant in debug mode
flutter run --flavor acme --dart-define=TENANT_ID=acme
# Build Android Release APK
flutter build apk --release --flavor acme --dart-define=TENANT_ID=acme
# Build Android Release AppBundle (Google Play)
flutter build appbundle --release --flavor acme --dart-define=TENANT_ID=acme
# Build iOS Release App
flutter build ios --release --flavor acme --dart-define=TENANT_ID=acme
๐ Recommended Folder Structure
Group tenant-specific logos and platform credentials under the root tenants/ folder:
my_flutter_app/
โโโ white_label.yaml # ๐ Central configuration for all tenants
โโโ tenants/ # ๐ Assets grouped per tenant
โ โโโ acme/
โ โ โโโ logo.png # ๐จ App logo / icon asset
โ โ โโโ firebase/ # ๐ Firebase credentials (optional)
โ โ โโโ google-services.json
โ โ โโโ GoogleService-Info.plist
โ โ
โ โโโ beta/
โ โโโ logo.png
โ โโโ firebase/
โ โโโ google-services.json
โ โโโ GoogleService-Info.plist
โ
โโโ lib/
โ โโโ main.dart
โ โโโ white_label.g.dart # โก Generated typed tenant constants
โโโ pubspec.yaml
โ๏ธ Configuration File (white_label.yaml)
Define all tenant properties in white_label.yaml:
white_label:
default_tenant: acme
tenants:
acme:
name: "Acme App"
version:
name: "1.0.0"
build_number: 1
android:
application_id: "com.example.acme"
app_name: "Acme App"
# version: # optional โ overrides the shared
# name: "1.0.0" # `version:` above for Android only, if
# build_number: 1 # this platform's release cadence diverges
ios:
bundle_id: "com.example.acme"
app_name: "Acme App"
# version: { ... } # same override shape as android.version
theme:
primary_color: "#1E88E5"
secondary_color: "#FFC107"
# brand_colors: { logo_accent: "#FF0000" } # optional, arbitrary
# feature_colors: { courses: "#00FF00" } # keyed hex-color maps
# section_colors: { header: "#0000FF" } # for apps whose UI
# gradient_colors: { start: "#111111" } # needs more than one
# # primary/secondary
environment: # the DEFAULT โ used whenever
api_base_url: "https://api.example.com" # `--env` isn't passed
# custom: # optional, arbitrary string
# sentry_dsn: "https://..." # key-values for anything else
# cdn_url: "https://cdn.example.com" # the build needs at runtime
environments: # optional โ NAMED overrides of the
staging: # `environment:` block above, selected
api_base_url: "https://staging-api.example.com" # via `--env`
custom: # (see ยง6 above).
sentry_dsn: "https://staging-sentry.example.com"
production:
api_base_url: "https://api.example.com"
# `staging`/`production` are just names picked for this example โ
# any key you write under `environments:` becomes a valid `--env`
# value (e.g. `qa`, `uat`, `demo` all work equally). A tenant that
# never declares `environments:` behaves exactly as before this
# existed โ the whole block is optional. Each named environment is
# an independent override, NOT a patch on top of `environment:` โ
# declare everything that environment needs.
features:
enable_push_notifications: true
enable_downloads: true
# icon_generate: true # see ยง5 above
# splash_generate: true
assets:
logo: "tenants/acme/logo.png"
# icon: "tenants/acme/icon.png" # optional
# splash: "tenants/acme/splash.png" # optional
firebase:
google_services_json: "tenants/acme/firebase/google-services.json"
google_service_info_plist: "tenants/acme/firebase/GoogleService-Info.plist"
๐ฑ Accessing Tenant Data in Flutter (Dart)
Access your active tenant's branding, API endpoints, and feature flags anywhere in your Dart code:
dart run white_label_kit:generate compiles the current build's tenant
(and, if --env was passed, that specific environment) into
lib/white_label.g.dart as a single whiteLabelRuntime constant (a
WhiteLabelRuntime) โ never a
map of every tenant, so no other tenant's data is ever compiled into a
build that isn't theirs:
import 'package:flutter/material.dart';
import 'white_label.g.dart';
void main() {
print('Tenant ID: ${whiteLabelRuntime.tenantId}');
print('App Name: ${whiteLabelRuntime.tenantName}');
print('Environment: $whiteLabelEnvironmentName'); // "" if --env wasn't used
print('API URL: ${whiteLabelRuntime.environment.apiBaseUrl}');
print('Sentry DSN: ${whiteLabelRuntime.environment.custom['sentry_dsn']}');
print('Primary Color: ${whiteLabelRuntime.theme.primaryColorHex}');
final hasPush = whiteLabelRuntime.isFeatureEnabled('enable_push_notifications');
print('Push Notifications: $hasPush');
runApp(const MyApp());
}
๐งฉ Optional: build_runner Integration
You do not need build_runner for anything above โ generate/
configure/build/run are plain, direct CLI commands, the same shape
as flutter_native_splash:create. If your app already runs dart run build_runner build for freezed/json_serializable/
injectable_generator and you'd like that same command to also
regenerate lib/white_label.g.dart, this package ships an optional
builder for it (lib/builder.dart) โ but it is not enabled
automatically, and needs two things added to your own project's
build.yaml before it does anything:
# your app's build.yaml
targets:
$default:
sources:
- white_label.yaml # lives at the project root, outside build_runner's default input set
- lib/** # default lib/** scan โ add it explicitly
builders:
white_label_kit:white_label_generator:
enabled: true # NOT auto-applied โ must opt in explicitly
Why this isn't auto-applied: without the sources: override above,
build_runner would activate the builder but it could never actually find
its white_label.yaml input โ while still treating
lib/white_label.g.dart as an output it owns, and deleting it on the next
build_runner build (the file generate/configure had already written
correctly gets wiped with no warning). If you don't need build_runner to
regenerate this file, don't add the build.yaml block above โ generate/
configure are unaffected either way.
๐ CLI Commands Reference
| Command | Description |
|---|---|
dart run white_label_kit |
Opens the interactive terminal runner & builder menu |
dart run white_label_kit:init [--example] [--force] [--path <dir>] |
Creates a starter white_label.yaml file |
dart run white_label_kit:add-tenant <id> "<Name>" <bundleId> [--logo <path>] [--default] |
Adds a new tenant and creates its asset directory |
dart run white_label_kit:update-tenant <id> [options] |
Updates tenant configuration fields |
dart run white_label_kit:remove-tenant <id> [--keep-assets] |
Removes the tenant's entry from white_label.yaml, deletes its tenants/<id>/ asset folder (unless --keep-assets), and cleans up its generated Android Gradle flavor, iOS Xcode build configs/scheme, and IDE run configurations |
dart run white_label_kit:generate [--tenant <id>] [--env <name>] [--config <path>] |
(Re)generates lib/white_label.g.dart for one tenant/environment |
dart run white_label_kit:configure [--tenant <id>] [--env <name>] [--platform android|ios|all] [--dry-run] [--skip-generate] |
Patches Android Gradle, iOS Xcode, IDE configs, icon/splash (if opted in), and regenerates lib/white_label.g.dart |
dart run white_label_kit:build [--tenant <id>] [--env <name>] [--platform android|android-aab|ios|all] [--mode debug|release] [--dry-run] [--clean] [--stage-only] |
Stages tenant assets, regenerates lib/white_label.g.dart, and invokes the real flutter build. --mode release always adds --obfuscate --split-debug-info=build/outputs/symbols/<tenant>/<platform> (see Flutter's obfuscation guide) โ not optional, so a release build can't accidentally ship un-obfuscated. |
dart run white_label_kit:run [--tenant <id>] [--env <name>] |
Stages tenant assets and regenerates lib/white_label.g.dart for a debug run |
dart run white_label_kit:validate |
Validates white_label.yaml syntax and asset paths |
dart run white_label_kit:list |
Lists all declared tenants and the default tenant |
dart run white_label_kit:doctor [id] [--all] [--json] [--strict] |
Performs a multi-tenant health check |
๐ Security
white_label.yaml and everything it generates (lib/white_label.g.dart,
native Gradle/Xcode config) end up baked into the built binary โ the
same way any other compiled Flutter asset does. Anyone who unzips a
shipped APK/IPA can read whiteLabelRuntime's data, including everything
under environment.custom/environments.*.custom.
Never put in white_label.yaml: signing keys/certificates, private
API secrets, database credentials, or anything else that would matter if
extracted from the built app. environment/environment.custom is for
public runtime config only (a base URL, a public DSN meant to be
client-visible, a CDN URL) โ not a place to smuggle a secret in because it
was convenient. Firebase's google-services.json/
GoogleService-Info.plist (via firebase:) are the one exception this
package handles directly, and only because Firebase itself designs those
files to ship inside the client app.
๐ซ What this does NOT do (yet)
Deliberate scope boundaries, not oversights โ flagged here instead of silently discovered later:
- No per-environment theme/icon/splash.
environments:/--env(ยง6) only ever changesenvironment(API URL +custom). Staging and production of the same tenant are expected to look identical; there is noenvironments.staging.themeor similar. If you need visually distinct staging builds, that's a real gap today, not a documented design choice โ open an issue rather than hand-rolling around it. environment/environments.*.customare flat string maps, not arbitrary JSON. Same deliberate constraint asfeatures(bool-only) โ richer/nested structured runtime content isn't modeled here.ASSETCATALOG_COMPILER_APPICON_NAMEandLAUNCH_SCREEN_STORYBOARD_NAME(the two remaining iOS Xcode keysicons_launcher/flutter_native_splashthemselves don't manage, beyond whatgenerateIosConfigsets) are not touched by this package at all โ seemaybeGenerateNativeSplash's storyboard-registration note in ยง5 for the one exception (the storyboard file reference).- No CI/CD orchestration. This package configures native files and generates Dart code; it does not run or generate pipeline definitions (Bitbucket/GitHub Actions/etc.) โ wire its CLI commands into whatever CI you already run.
๐ค Contributing
Contributions, issues, and feature requests are welcome! Feel free to check the issues page.
๐ License
This project is licensed under the MIT License โ see the LICENSE file for details.
Libraries
- builder
- OPTIONAL, advanced alternative to
dart run white_label_kit:generate(seebin/generate.dartโ that's the primary, recommended path, the same wayflutter_native_splash:create/icons_launcher:creatework: one direct command, nobuild_runnerinvolved). If your project already runsdart run build_runner buildforfreezed/json_serializable/injectable_generator, this lets that same command also regeneratelib/white_label.g.dartโ but it requires abuild.yamlsources:override (white_label.yamlsits at the project root, outsidebuild_runner's defaultlib/**scan) thatgenerate/initdo not. Both paths call the exact same generation logic (lib/src/generation/dart_config_generator.dart) โ pick whichever fits your project, they produce identical output. - white_label_kit
- Unified white-label build platform for Flutter apps.