Mobile
Get the iOS and Android apps ready for the stores: the release env file, the URL scheme, verified links and OAuth redirects, Android's release signing and the iOS team and bundle id, then Flutter's release guides.
The kit's share of a mobile release is configuration: the values a store build reads, the identifiers that stores and sign-in providers match against, and a real signing key on Android. This page covers each one. Signing, build numbers, store listings and review are the same for every Flutter app, so the last section hands you to Flutter's own release guides.
Read Going to production first, and deploy the backend before
any build that points at it. The examples use the identifiers from
floot create my_app --org com.acme, and api.myapp.com for a self-hosted
backend.
The release .env
Store builds read .env, bundled as an asset; set it up once as
Building for release shows. One file
serves both platforms, and the web build too.
| Key | In a store build |
|---|---|
SUPABASE_URL | https://<ref>.supabase.co, or your own API host such as https://api.myapp.com; never 127.0.0.1, 10.0.2.2 or a LAN address |
SUPABASE_PUBLISHABLE_KEY | Your project's publishable key, sb_publishable_…, not the local stack's |
OAUTH_REDIRECT_URL | Unchanged: <your scheme>://oauth-callback |
WEB_APP_URL | Your web app's origin, https://app.myapp.com. Stripe's returns go there, and open the app (see Deep-link domains) |
OAUTH_GOOGLE_IOS_CLIENT_ID, OAUTH_GOOGLE_WEB_CLIENT_ID | The client ids of your production Google project |
REVENUECAT_PUBLIC_APPLE_KEY, REVENUECAT_PUBLIC_ANDROID_KEY | The public SDK keys of your production app configs, appl_… and goog_… |
PRIVACY_POLICY_URL, TERMS_OF_USE_URL | Your hosted documents. Both stores ask for a privacy policy URL as well |
ENABLE_* | The features this release ships. ENABLE_MULTI_TENANCY, ENABLE_SEAT_BASED_BILLING and ENABLE_REVENUECAT also exist as function secrets: keep each equal on both sides |
A store build is public
Anyone who installs the app can unzip it and read .env, and .env.local
beside it, since pubspec.yaml bundles both. Put nothing in either file that
you would not publish.
A store build keeps its values for as long as it stays installed, and people update late or never. Moving the backend to a new URL, or retiring its publishable key, strands every copy built with the old value. Ship a URL you intend to keep.
The URL scheme and redirects
Your identifiers are permanent
The kit's deep links use a custom URL scheme, com.acme.myapp, which
floot create built from --org and your project name, alongside the iOS
bundle id com.acme.myApp and the Android package name com.acme.my_app
(see Deep Links).
Once a build is in a store, its bundle id and package name can never change. So settle the organization before the first upload:
- Created without
--org? Everything starts withcom.example, and Google Play refuses to publish a package name incom.example. - Changing it means the scheme in the six places
Deep Links lists, the bundle id in Xcode,
applicationIdinandroid/app/build.gradle.kts, and every provider and store entry made with the old value.
The app's callback on the backend
The app's callback, com.acme.myapp://oauth-callback, must be on your
hosted project's redirect allow-list, which config.toml does not reach: on
Supabase Cloud under Redirect URLs (see
URLs), self-hosted in
GOTRUE_URI_ALLOW_LIST (see
Auth defaults that differ).
Deep-link domains
Stripe's returns and invitations are https links on your web app's domain,
and they open the installed app once each platform has verified the domain:
Universal Links on iOS and macOS, App Links on Android (see
Verified links). floot create --web-domain
already wrote the domain into the Android intent filter and WEB_APP_URL;
copy WEB_APP_URL into the release .env above. Four things are left to you.
Each platform proves you own the domain by fetching a file from it,
/.well-known/apple-app-site-association for Apple and
/.well-known/assetlinks.json for Android. Both ship in web/.well-known/,
and flutter build web copies them into the build, so deploying the web app
serves them, as long as the host follows
Hosting requirements. Until the
values below are in them, verification fails and every link opens the web app
instead, which still works.
iOS: Associated Domains in Xcode
Add the capability at the same visit as the team, in iOS: team and bundle id, the way you add Sign in with Apple:
- Open
ios/Runner.xcworkspacein Xcode, then Runner under TARGETS → Signing & Capabilities. - Click + Capability and add Associated Domains.
- Under Domains, add
applinks:app.myapp.com: your web domain, with no scheme or path.
Xcode writes it to ios/Runner/Runner.entitlements, beside Sign in with Apple
if you added it. With Automatically manage signing on, Xcode turns the
capability on for the App ID and refreshes the provisioning profile itself;
otherwise turn on Associated Domains for the App ID in your developer
account, and download the new profile.
The Apple file: your team ID
appIDs names your app as <team ID>.<bundle id>. floot create filled in
the bundle id; replace APPLE_TEAM_ID with your ten-character Team ID, from
Membership details in your Apple Developer account:
"appIDs": ["ABCDE12345.com.acme.myApp"],The same entry covers the Mac app, which has the same bundle id.
The Android file: your signing fingerprints
Android checks the SHA-256 fingerprint of the certificate that signed the
installed app. Replace ANDROID_SIGNING_CERT_SHA256 with your upload key's,
from keytool, as it prints it (colon-separated, upper case):
keytool -list -v -keystore ~/upload-keystore.jks -alias upload"sha256_cert_fingerprints": [
"14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
]Once the app is on Google Play, add a second entry: Play's app signing key,
from Play Console's app signing page. Every install from the store is signed
with that key, not yours (see
Android: replace the debug signing), so
without it links open the browser for every store user while working on your
own device. A debug build on your machine needs the debug key's fingerprint
too (keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android).
macOS opt-in
A new project's Mac app claims no domain, so its links land on the web app,
as on Windows and Linux. Associated Domains needs a development team and
development signing, and with the entitlement but no team every macOS build
fails ("has entitlements that require signing with a development
certificate"). So floot create leaves the entry out, and the first macOS
build works with no Apple account. To opt in:
-
Add the entry to both
macos/Runner/DebugProfile.entitlementsandmacos/Runner/Release.entitlements:macos/Runner/Release.entitlements <key>com.apple.developer.associated-domains</key> <array> <string>applinks:app.myapp.com</string> </array> -
Open
macos/Runner.xcworkspacein Xcode, then Runner under TARGETS → Signing & Capabilities. With Automatically manage signing on, pick your Team, and set Signing Certificate to Development: the target otherwise keeps Sign to Run Locally, the ad hoc signing Associated Domains refuses. -
Build once from Xcode (Product → Build), which creates the Mac provisioning profile with the capability.
flutter run -d macosandflutter build macoswork from then on.
The app already hands the link to Flutter: macos/Runner/AppDelegate.swift
ships with the override app_links needs. Even opted in, a Mac whose default
browser is Chrome, Arc or Edge opens the web app: Chromium browsers do not
hand Universal Links to apps on macOS.
Once a build is installed, the troubleshooting entry checks each link in the chain, in order.
OAuth redirect URIs, per platform
On mobile, Google and Apple sign-in are native where the platform offers it, and every other sign-in goes through the browser:
| Provider | iOS | Android |
|---|---|---|
| Apple | Native sheet, matched on the bundle id | Browser, through your Apple Services ID |
| Native, matched on the iOS client's bundle id | Native, matched on package name and SHA-1 | |
| GitHub | Browser | Browser |
A browser sign-in makes a round trip through your backend:
Two different URLs, registered in two different places:
- The provider's redirect URI is your backend's:
https://<ref>.supabase.co/auth/v1/callback, orhttps://api.myapp.com/auth/v1/callbackwhen self-hosted. Register it with GitHub, Google's Web client and Apple's Services ID, replacing the local or tunnelled one you used in development: GitHub, Google, Apple. - The backend's redirect is the app's callback, which only the allow-list above has to know.
The native flows have no redirect at all; they match the app's identity instead:
- Google on iOS checks the bundle id on your iOS client. Its reversed
client id must replace the
your_google_ios_REVERSED_client_idplaceholder inios/Runner/Info.plist: see Google Sign-In. - Apple on iOS checks the App ID, with Sign in with Apple enabled on it, and your bundle id among the provider's Client IDs: see Create App ID.
- Google on Android checks the package name and the SHA-1 of the certificate that signed the installed build. A store build is not signed with your debug key, so a client registered only for debug fails in production while working on your machine. The next section covers it.
Android: replace the debug signing
flutter create leaves the release build signed with the debug key, a TODO
in android/app/build.gradle.kts:
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}Google Play rejects a bundle signed with a debug key, and a debug key is no identity: it differs on every machine. Create an upload keystore, keep it and its passwords out of the repository and backed up, and point the release build at it, as Flutter's Sign the app guide shows.
With Play App Signing, the default for new apps, Google re-signs what you upload with its own app signing key, so three certificates sign builds that people run: your debug key locally, your upload key on release builds you install yourself, and Play's app signing key on every install from the store. Google Sign-In needs an Android OAuth client for each one it should work on, each with its own SHA-1 (see Google Sign-In). Play Console shows the app signing key's fingerprints on its app signing page.
iOS: team and bundle id
Open ios/Runner.xcworkspace in Xcode, select Runner under TARGETS,
then Signing & Capabilities:
- Team: the Apple Developer team that publishes the app.
flutter createfills in the team of a signing certificate it finds on the machine that ran it, if any, which may be a personal team; check it. - Bundle Identifier:
com.acme.myApp. Register it as an App ID in that team, and use the same value in the Google iOS client, the Apple provider's Client IDs, App Store Connect and the backend'sOAUTH_APPLE_CLIENT_IDfunction secret (see The Apple client id). - Capabilities: Associated Domains, for verified links (see iOS: Associated Domains in Xcode), Sign in with Apple if you offer it, and In-App Purchase if you sell through RevenueCat.
Store apps and products
In-app purchases need an app in each store, with its products, before RevenueCat can import them. Store setup covers what is specific to your project: the bundle id and package name to use, the capability, the server notification URL and the service account.
Flutter's release guides
From here a Floot app releases like any other Flutter app. Follow Flutter's guides for signing, version and build numbers, building the bundle or archive, the store listing and review:
Before you submit, install a release build on a real device, pointed at the live backend, and run the smoke check from it, including each social sign-in you offer.