A start-to-finish guide for connecting an existing Flutter app, preserving its native identity, configuring profiles and flavors, and shipping mobile, web, push, deep links, symbols, and Shorebird OTA with Mach.
What adding Mach changes — and what it does not
This guide starts with a Flutter application that already exists. Mach adds a small mach.config.json file and connects the repository to a Mach Dashboard project. It does not regenerate lib, android, or ios, replace Gradle or CocoaPods configuration, rename your application, or move your source files.
During a build, Mach runs the real Flutter toolchain from your configured app directory. Flutter resolves packages, Gradle creates Android artifacts, and CocoaPods plus Xcode create iOS artifacts. Metro, Expo prebuild, and JavaScript package installation never enter the Flutter pipeline.
Supported targets: standalone Flutter Android, iOS, and web applications.
Supported layouts: an app at the repository root or below a Dart workspace root.
Preserved: bundle identifiers, package names, flavors, schemes, entrypoints, plugins, native code, and the lockfile.
Not currently supported: Flutter add-to-app modules whose artifact is owned by a separate host application.
Safe for an existing app
Mach orchestrates the release pipeline around the native projects you already own. Integration does not require rewriting the Flutter application.
1. Confirm the existing app is ready
Begin from a clean Git commit and make sure the same Flutter SDK you want Mach to pin can resolve and validate the project. Fix existing Flutter or native build failures before introducing a second build environment; that keeps integration problems easy to identify.
Mach CLI requires Node.js 18 or newer, even though the Flutter build itself does not use Node. A local Android SDK is optional for cloud Android builds. Xcode is required only for local iOS builds; managed iOS builds use Mach's macOS runner.
A Mach account and permission to create or link the Dashboard project.
The Flutter SDK version the team intends to use, available as flutter in the current shell.
Committed pubspec.yaml and pubspec.lock files for the application.
The existing Android applicationId and iOS PRODUCT_BUNDLE_IDENTIFIER.
Apple Developer or Google Play access only when you are ready to sign or submit releases.
Validate the current project first
git status --short
flutter --version
flutter doctor -v
flutter pub get
flutter analyze
flutter test
Keep a rollback point
Commit or stash unrelated work before integration. A clean diff makes every generated configuration change reviewable.
2. Install Mach and sign in
Install the latest Mach CLI globally, verify the command, and authenticate. Interactive login stores the Mach session on this computer. CI uses a project-authorized MACH_TOKEN instead of an interactive login.
node --version
npm install -g @radhya/mach@latest
mach --version
mach login
mach me
3. Create or link the Mach project
Run Mach from the repository root, not from android or ios. Choose mach init when this application does not yet have a Mach Dashboard project. It detects the Flutter app, creates the Dashboard project, writes mach.config.json, reads the existing native identifiers, and pins the active Flutter SDK.
Choose mach link when someone has already created the project in the Dashboard. Link lets you select that project and safely merges its identity into an existing mach.config.json. Existing profile names, inheritance, and custom fields are preserved.
cd /path/to/existing-flutter-repository
# New Mach Dashboard project
mach init
# OR: connect to a project that already exists
mach link
Do not run both flows
init creates a project; link selects an existing one. Keep the generated projectId unchanged because it connects builds, credentials, secrets, and submissions to the Dashboard.
4. Review the generated source of truth
Open mach.config.json immediately after init or link. framework must be flutter, appDirectory must point to the folder containing the application's pubspec.yaml, and flutter.sdkVersion should match flutter --version. Mach creates development, staging, and production profiles, but profile names are fully customizable.
The profile name itself has no hidden meaning. environment chooses Dashboard values, distribution chooses signing and artifact intent, and the nested flutter fields choose the Flutter invocation.
development uses development signing on iOS.
internal creates an installable testing artifact: Android defaults to APK and iOS uses ad hoc signing.
store creates a release artifact: Android defaults to AAB and iOS uses App Store signing.
Any number of uniquely named profiles can extend and override one another.
{
"projectId": "<keep-the-id-generated-by-mach>",
"framework": "flutter",
"appDirectory": ".",
"flutter": { "sdkVersion": "3.47.1" },
"ios": {
"bundleIdentifier": "com.acme.customerapp",
"buildNumber": "auto"
},
"android": {
"package": "com.acme.customerapp",
"versionCode": "auto"
},
"build": {
"development": {
"environment": "development",
"distribution": "development"
},
"staging": {
"environment": "staging",
"distribution": "internal"
},
"production": {
"environment": "production",
"distribution": "store"
}
}
}
Preserve generated identity
The placeholder above is explanatory. Keep the real projectId, name, slug, identifiers, and SDK version written for your project.
5. Preserve the app's existing identity
For an app already installed by users or registered in a store, its package name and bundle identifier are permanent identity, not display labels. Confirm that Mach detected the same Android applicationId and iOS PRODUCT_BUNDLE_IDENTIFIER already used by the project and stores.
Do not replace a production identifier just to make the two platforms match. Android and iOS identifiers may differ, and staging identifiers may differ from production when existing flavors are separate apps.
Android: compare android.package with the resolved applicationId in android/app/build.gradle or build.gradle.kts.
iOS: compare ios.bundleIdentifier with the Runner target's PRODUCT_BUNDLE_IDENTIFIER in Xcode build settings.
If a flavor changes either identifier, put the override inside that build profile.
Credentials and native version counters are scoped to the resolved identifier.
mach.config.json identity fields
{
"ios": {
"bundleIdentifier": "com.acme.customerapp",
"buildNumber": "auto"
},
"android": {
"package": "com.acme.customerapp.android",
"versionCode": "auto"
}
}
Published app? Never guess
Copy identifiers from the working native targets or store records. Changing them creates a different app and makes existing signing material unusable.
6. Pin the Flutter SDK deliberately
Mach managed runners reproduce the exact flutter.sdkVersion stored at the config root. The runner locates that version in Flutter's official archive manifest, verifies its SHA-256 checksum, and keeps its cache isolated by project, framework, and SDK.
Local builds are strict too. If the shell activates a different Flutter version, Mach stops and asks you to activate the configured SDK or update the pin intentionally. Teams using FVM should make sure the flutter command visible to Mach resolves to the project SDK.
flutter --version
mach config --profile staging
# After intentionally upgrading Flutter
flutter pub get
flutter analyze
flutter test
# Then update flutter.sdkVersion and commit it
7. Map existing flavors and Dart entrypoints
Mach selects flavors that already exist in the Flutter native projects; it does not invent product flavors or Xcode schemes. Use flutter.flavor when Android has that product flavor and iOS has a shared scheme with the same name. Use flutter.target when the profile has a dedicated Dart entrypoint. An unflavored app can omit both fields because target defaults to lib/main.dart.
Flutter's default-flavor in pubspec.yaml is honored when a profile does not specify one. If Android declares flavors but has no default, Mach fails the audit before allocating a cloud runner and asks for an explicit flavor.
The target path is relative to appDirectory and must stay inside it.
For iOS, the flavor should resolve to a shared scheme and matching Debug-, Profile-, or Release-flavor configuration.
Use debug for developer builds, profile for performance profiling, and release for distribution.
Keep staging and production identifiers aligned with the native flavor configuration already in the app.
{
"build": {
"team-staging": {
"environment": "staging",
"distribution": "internal",
"ios": { "bundleIdentifier": "com.acme.customerapp.staging" },
"android": { "package": "com.acme.customerapp.staging" },
"flutter": {
"flavor": "staging",
"target": "lib/main_staging.dart",
"mode": "release",
"dartDefinesFromEnv": ["API_URL", "APP_ENV"]
}
},
"store-release": {
"environment": "production",
"distribution": "store",
"ios": { "bundleIdentifier": "com.acme.customerapp" },
"android": { "package": "com.acme.customerapp" },
"flutter": {
"flavor": "production",
"target": "lib/main_production.dart",
"mode": "release",
"dartDefinesFromEnv": ["API_URL", "APP_ENV"]
}
}
}
}
8. Handle different Android and iOS flavor names
Some existing apps use lowercase Android flavors but title-cased or different Xcode schemes. Keep common Flutter fields at the profile level, then place a flutter override inside android or ios. Platform values override shared profile values.
Set ios.iosConfiguration only when the Xcode project uses a nonstandard configuration name that Mach cannot derive safely from the scheme and mode.
Platform-specific overrides
{
"build": {
"preprod-client": {
"environment": "staging",
"distribution": "internal",
"flutter": {
"target": "lib/main_preprod.dart",
"mode": "release",
"dartDefinesFromEnv": ["API_URL", "APP_ENV"]
},
"android": {
"package": "com.acme.customerapp.preprod",
"flutter": { "flavor": "preprod" }
},
"ios": {
"bundleIdentifier": "com.acme.customerapp.preprod",
"iosConfiguration": "Release-PreProd",
"flutter": { "flavor": "PreProd" }
}
}
}
}
9. Connect Dashboard environments to Dart defines
A profile's environment selects the corresponding Dashboard environment values. flutter.dartDefinesFromEnv is an allowlist of names that Mach forwards to flutter build as --dart-define KEY=value. The profile therefore resolves the intended configuration locally and on a managed runner without hardcoding endpoint values in the command.
Add environment-scoped values from the project's Secrets page in the Dashboard. mach env set is useful for global values, while mach env list confirms which names exist without revealing protected values.
Good Dart defines: API base URL, application environment, analytics mode, or public feature switches.
Never use Dart defines for passwords, private keys, signing material, service-account JSON, or bearer tokens.
A missing allowlisted value is a hard failure, preventing a silently misconfigured artifact.
Credential files are managed through mach credentials, not dartDefinesFromEnv.
# Confirm configured names and their scopes
mach env list
# Optional: add a non-environment-specific value globally
mach env set SUPPORT_EMAIL=support@example.com
mach config --profile team-staging
Compiled configuration is not secret storage
Mach encrypts Dashboard values in storage and transport, but any value passed to --dart-define becomes part of the application binary. Treat it as public runtime configuration.
10. Reduce duplication with profile inheritance
Large applications can have dozens of customer or regional profiles. Use extends to share distribution, mode, and the Dart-define list, then override only the environment, native identifiers, target, or flavor that differs. Mach resolves the complete profile before building and rejects circular inheritance.
{
"build": {
"internal-base": {
"distribution": "internal",
"flutter": {
"mode": "release",
"dartDefinesFromEnv": ["API_URL", "APP_ENV"]
}
},
"india-qa": {
"extends": "internal-base",
"environment": "staging",
"android": { "package": "com.acme.app.in.qa" },
"ios": { "bundleIdentifier": "com.acme.app.in.qa" },
"flutter": {
"flavor": "indiaQa",
"target": "lib/main_india.dart"
}
}
}
}
11. Audit before spending a build minute
mach audit combines doctor-style setup checks with project, dependency, security, permission, and release-impact checks. For Flutter it validates the SDK pin and Dart constraint, runs Flutter doctor and static analysis, checks the app lockfile, validates targets and declared Android flavors, and inspects native manifests when available.
mach config shows the final merged profile including inherited and platform-specific values. Use both commands after every meaningful profile change and in CI before a release build.
mach audit
mach config --profile team-staging
mach config --profile store-release --json
# CI-friendly audit output
mach audit --json --severity high
Fail before infrastructure starts
Invalid Flutter targets, missing flavors, unsafe appDirectory values, and toolchain mismatches stop locally rather than consuming a cloud runner.
12. Prove the profile with a local build
A Mach local build runs the same resolved profile and Flutter arguments on your computer. It is the fastest way to verify a new flavor mapping before cloud delivery. Android local builds require the Android SDK; iOS local builds require macOS, Xcode, CocoaPods, and appropriate signing access.
mach start respects appDirectory, target, flavor, mode, Dart defines, and the pinned SDK. It is useful for a profile-aware development launch, while mach build --local produces the artifact.
mach start --profile team-staging
mach build --platform android --profile team-staging --local
# Run only on a configured Mac
mach build --platform ios --profile team-staging --local
13. Configure signing for each release identity
Run mach credentials with the same profile you intend to build. Mach resolves that profile's package or bundle identifier and distribution, then guides you through the matching credential. A staging identity and a production identity are different credential targets when their identifiers differ.
Android: create or upload the keystore for the resolved package and distribution.
iOS development: use development signing for registered developer devices.
iOS internal: use ad hoc signing and register every test device that must install the IPA.
iOS store: use an Apple Distribution certificate and App Store provisioning profile.
Mach injects credentials only for the build and keeps them out of the repository and Flutter source.
mach credentials --profile team-staging
mach credentials --profile store-release
14. Seed native versions for an already-published app
When versionCode and buildNumber are auto, Mach allocates the next Dashboard-owned native counter and passes it to Flutter as the build number. Before the first Mach production build of an existing store app, seed each counter with the latest number already uploaded to that store. The next build uses the following value.
The marketing version still comes from pubspec.yaml. Native counters exist to make each uploaded Android or iOS binary unique.
Example: latest Play versionCode 42, latest App Store build 108
mach version seed --platform android --profile store-release --value 42
mach version seed --platform ios --profile store-release --value 108
mach version show --platform android --profile store-release
mach version show --platform ios --profile store-release
Seed the latest used value
Do not seed the number you want next. Seed the latest number already consumed by Google Play or App Store Connect; Mach increments it for the next build.
15. Run managed Android, iOS, and web builds
After audit and credentials are ready, the command is identical on every developer machine and in CI. Mach uploads a safe source snapshot, registers the build, activates the pinned Flutter SDK, restores isolated caches, injects the selected environment and temporary signing material, runs Flutter, validates the artifact, and stores it in the Dashboard.
Android internal or development builds produce APK files by default; store builds produce AAB files. Set apk explicitly only when you need to override that behavior. iOS uses Mach's certificate, provisioning-profile, archive, validation, and IPA pipeline. Web builds run flutter build web and publish a downloadable flutter-web.zip without requiring mobile signing.
# Installable team builds
mach build --platform android --profile team-staging
mach build --platform ios --profile team-staging
# Store-ready builds
mach build --platform android --profile store-release
mach build --platform ios --profile store-release
# Compiled web bundle
mach build --platform web --profile store-release
The Dashboard keeps the release context
Each build records framework, SDK and Dart versions, profile, flavor, target, mode, native version, logs, signing status, and downloadable artifacts.
16. Keep production stack traces decodable
Flutter release obfuscation is useful only when its matching symbol map survives the build. Enable obfuscate and splitDebugInfo together in the profile. Mach passes the release flags to Flutter, archives the generated directory as flutter-symbols.zip, and keeps it beside the application artifact in the build details.
Treat the symbol archive as a release artifact. Download and retain the exact archive for every production build; a map from a different build cannot reliably decode that stack trace.
Production symbol configuration
{
"build": {
"store-release": {
"distribution": "store",
"flutter": {
"mode": "release",
"obfuscate": true,
"splitDebugInfo": ".mach/flutter-symbols/store-release"
}
}
}
}
Symbols are private operational data
Do not package them into the public app. Keep access limited to the people and systems that decode production crashes.
17. Add Mach Push without taking ownership from the app
mach push setup installs the Flutter client, configures the existing Android and iOS notification projects, writes generated transport configuration, and adds one explicit non-blocking startup call to the selected Dart entrypoint. An app-owned lib/mach_push.dart file holds navigation and foreground callbacks and is preserved on later setup runs.
The client registers APNs or FCM tokens against the same Mach Push API used by other frameworks, keeps a stable installation identifier, and re-registers after token rotation. Notification permission remains an application UX decision, so request it at an appropriate moment rather than automatically on launch.
mach push setup --profile team-staging
flutter analyze
# Review these app-owned integration points
# lib/mach_push.dart
# lib/mach_push.generated.dart
18. Configure and verify Flutter deep links
Mach configures the native routing contract while your Flutter router decides which screen to display. On Android it writes the verified HTTPS intent filter and optional custom scheme. On iOS it writes Associated Domains entitlements, connects the entitlement file to the Runner target, and registers the custom scheme in Info.plist.
The command is idempotent: rerunning it updates Mach-owned declarations without duplicating URL types, intent filters, or entitlement values. Verify also inspects both native projects so a missing association is caught before release.
mach deeplink setup --profile team-staging --base-url https://links.example.com --scheme acme
mach deeplink verify --profile team-staging
19. Use customer-owned Shorebird for Flutter OTA
Flutter OTA uses Shorebird rather than the Expo Updates runtime. mach ota setup initializes shorebird.yaml, a base release establishes the native-compatible release, and publish creates a patch for that release. Mach resolves the selected custom profile so its flavor, target, and allowlisted Dart defines are forwarded consistently.
Authentication stays customer-owned: store SHOREBIRD_TOKEN in the CI environment or secret provider. Mach neither proxies patch files nor adds a per-update infrastructure charge. Native code changes, newly declared native assets, or other changes outside Shorebird's patch contract require a new base release.
# Create an API key in Shorebird Console → Account → API Keys
# Save it as the protected SHOREBIRD_TOKEN secret
mach ota setup --profile store-release
mach ota release --platform android --profile store-release
mach ota release --platform ios --profile store-release
mach ota publish --platform all --profile store-release --track stable
mach ota list
No Mach OTA hosting bill
Shorebird serves the Flutter patches under the customer's Shorebird account. Any Shorebird plan limits belong to that account, not to Mach infrastructure.
20. Connect store service credentials and submit
Signing credentials prove who built the app; store service credentials authorize the upload. For Google Play, attach a Play service-account JSON file to the production package. For iOS, add the App Store Connect API key and make sure the production profile resolves the App Store Connect app and Apple team metadata.
Use --latest together with --profile so Mach selects only the latest successful build from that profile. This prevents a staging artifact from being chosen simply because it finished more recently.
mach credentials:service --platform android --profile store-release --file ~/Downloads/play-service-account.json
mach credentials:service --platform ios --profile store-release
mach submit --latest --platform android --profile store-release --track internal
mach submit --latest --platform ios --profile store-release
21. Integrate a Flutter app inside a monorepo
Keep mach.config.json at the source snapshot root and set appDirectory to the Flutter application folder. Mach uploads the repository root so workspace packages remain available, but runs Flutter and native validation from appDirectory. The path must be relative, stay inside the repository, and contain pubspec.yaml.
Commit the application lockfile and any Dart workspace files required to resolve local packages. Do not place mach.config.json inside android or ios.
Repository-root mach.config.json
{
"framework": "flutter",
"appDirectory": "apps/mobile",
"flutter": { "sdkVersion": "3.47.1" }
}
22. Add the verified profile to CI
Create a named access token in Dashboard Settings and store it in the CI provider as MACH_TOKEN. Never commit the token. Install the CLI, run the audit, then build with explicit platform and profile values. JSON output provides a machine-readable handoff to later pipeline steps.
export MACH_TOKEN="$MACH_TOKEN"
npm install -g @radhya/mach@latest
mach audit --severity high
mach build --platform android --profile store-release --json-output mach-android-build.json
mach submit --latest --platform android --profile store-release --track internal
Keep CI deterministic
Commit mach.config.json and pubspec.lock, use an explicit profile, and keep the pinned Flutter SDK under code review. The runner then reproduces the toolchain without machine-specific setup.
23. Troubleshoot common integration failures
Start with mach audit and mach config --profile <name>. Most Flutter integration failures are configuration mismatches that can be fixed before another build: the active SDK differs from the pin, the target path is wrong, a flavor does not exist, an environment value is missing, or signing belongs to a different identifier or distribution.
SDK mismatch: activate flutter.sdkVersion locally, or upgrade the pin intentionally after the project passes Flutter checks.
No matching flavor: confirm the Android product flavor and iOS shared scheme, including capitalization; add platform-specific overrides when names differ.
Missing Dart define: add the named value to the profile's Dashboard environment or remove it from dartDefinesFromEnv.
Credential not found: run mach credentials with the same profile and confirm its resolved identifier and distribution.
Wrong artifact type: internal defaults to APK and store defaults to AAB; review distribution and any explicit apk override.
Store rejects version: seed the latest already-used native counter, then rebuild with auto versioning.
Workspace package missing: run from the repository root and confirm appDirectory plus committed Dart workspace files.
Cloud-only failure: compare Dashboard Flutter metadata with mach config and inspect the first failing build phase, not only the final summary.
mach audit
mach config --profile team-staging
flutter --version
flutter doctor -v
flutter analyze
24. Know the current Flutter boundary
Mach supports Flutter builds for Android, iOS, and web; native signing; automatic versioning; dependency and project audits; local or managed execution; artifacts and debug symbols; Maestro tests on mobile; native deep-link setup; Mach Push; customer-owned Shorebird OTA; and Android and iOS store submission.
Flutter add-to-app host applications remain outside the current build contract because the host application owns their final artifact. Expo Router sitemap generation and Expo config-plugin automation remain Expo-specific; the CLI stops early instead of applying a JavaScript framework tool to Flutter.
Final integration checklist
The integration is complete when another developer or CI runner can clone the repository, authenticate to Mach, inspect the same resolved profile, and produce the expected signed artifact without recreating local machine state.
mach.config.json is committed with framework: flutter and the correct appDirectory.
flutter.sdkVersion matches the tested team SDK and pubspec.lock is committed.
Every profile resolves the correct environment, distribution, identifiers, flavor, target, and mode.
dartDefinesFromEnv contains configuration names only and no sensitive application secret.
mach audit passes and local profile smoke tests are complete.
Signing credentials match each package or bundle identifier and distribution.
Existing store counters were seeded before auto versioning was used.
Managed artifacts appear in the Dashboard with expected metadata and can be installed or uploaded.
Web builds produce flutter-web.zip and obfuscated releases retain their matching flutter-symbols.zip.
Push callbacks, native deep-link declarations, and Shorebird base-release and patch flows are tested for the selected profile.
mach audit
mach config --profile store-release
mach build --platform android --profile store-release
mach build --platform ios --profile store-release
mach build --platform web --profile store-release