Flootdocs

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.

KeyIn a store build
SUPABASE_URLhttps://<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_KEYYour project's publishable key, sb_publishable_…, not the local stack's
OAUTH_REDIRECT_URLUnchanged: <your scheme>://oauth-callback
WEB_APP_URLYour 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_IDThe client ids of your production Google project
REVENUECAT_PUBLIC_APPLE_KEY, REVENUECAT_PUBLIC_ANDROID_KEYThe public SDK keys of your production app configs, appl_… and goog_…
PRIVACY_POLICY_URL, TERMS_OF_USE_URLYour 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 with com.example, and Google Play refuses to publish a package name in com.example.
  • Changing it means the scheme in the six places Deep Links lists, the bundle id in Xcode, applicationId in android/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).

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.xcworkspace in 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:

web/.well-known/apple-app-site-association
"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):

Terminal
keytool -list -v -keystore ~/upload-keystore.jks -alias upload
web/.well-known/assetlinks.json
"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:

  1. Add the entry to both macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements:

    macos/Runner/Release.entitlements
    <key>com.apple.developer.associated-domains</key>
    <array>
        <string>applinks:app.myapp.com</string>
    </array>
  2. Open macos/Runner.xcworkspace in 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.

  3. Build once from Xcode (Product → Build), which creates the Mac provisioning profile with the capability. flutter run -d macos and flutter build macos work 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:

ProvideriOSAndroid
AppleNative sheet, matched on the bundle idBrowser, through your Apple Services ID
GoogleNative, matched on the iOS client's bundle idNative, matched on package name and SHA-1
GitHubBrowserBrowser

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, or https://api.myapp.com/auth/v1/callback when 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_id placeholder in ios/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:

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 create fills 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's OAUTH_APPLE_CLIENT_ID function 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.

On this page