Skip to main content

Firebase

Five modules bring Firebase into an app:

ModuleWhat it adds
firebase_coreFirebase itself: firebase_core, the options of the Firebase app, and its initialization at start-up.
firebase_crashlyticsCrash reporting with Firebase Crashlytics.
firebase_analyticsAnalytics with Firebase Analytics, including screen views.
firebase_authSign-in with Firebase Authentication: an email address and a password, an anonymous user, and the ways to sign in of Google and Apple. The feature sign_in, the screens of sign-in, brings it.
firebase_messagingPush messages with Firebase Cloud Messaging: the address of the device for the backend of the app, the messages and the taps on them.

The last four depend on firebase_core, which smf create adds with them.

Some of the setup is out of reach of any command, in the Firebase console and in your Apple Developer account: Google Analytics in the project, the APNs key of Cloud Messaging, Apple in Firebase Authentication, and the SHA of the keys of a release for Google. These are steps by hand. The README of the app lists them under "Finish the setup", and smf create ends with a link to their guides, which show each one click by click. They repeat for each Firebase project that your flavors use; see what repeats.

What SMF generates​

firebase_core gives the app a file of options for each flavor, lib/firebase_options_dev.dart, lib/firebase_options_staging.dart and lib/firebase_options_prod.dart. firebaseOptionsOfFlavor in lib/firebase_options_of_flavor.dart picks the options by the flavor of the build, and bootstrap() initializes Firebase with them before the other services:

await Firebase.initializeApp(options: firebaseOptionsOfFlavor);

appFlavor is a constant, so a release build keeps only the options of its own flavor.

The options come from your Firebase project, where the FlutterFire CLI registers the apps. Until a flavor is configured, its file is a placeholder in the form that flutterfire writes, so the app compiles but a build of that flavor stops at start-up with an UnsupportedError that gives the command.

The module also adds the script tool/configure_firebase.dart, which configures one flavor, and asks for iOS 15.0 or newer, which the Firebase SDKs need and the app has already. It adds a Firebase section to the README of the app with the commands below.

Before generation: the machine​

The FlutterFire CLI needs a few tools, so SMF checks the machine before it generates an app with Firebase:

CheckWhat it looks for
Firebase CLIThe firebase command, which the FlutterFire CLI runs to reach your Firebase projects. SMF runs firebase --version to see that it works.
Firebase loginAn account logged in to the Firebase CLI, as firebase login:list --json reports it. SMF then lists your Firebase projects with firebase projects:list --debug to see that Google still accepts the login.
FlutterFire CLIflutterfire_cli 1.4.1 or a later 1.x, activated globally, as dart pub global activate flutterfire_cli 1.4.1 does.
Xcode project toolsOn macOS, Ruby with the gem xcodeproj 1.23.0 or newer, with which the FlutterFire CLI sets up the iOS app in its Xcode project.
Setup of the Xcode project on a MacOn other systems, a warning: the FlutterFire CLI changes the Xcode project only on macOS.
Firebase CLI 15.34.0 or laterFor an app with firebase_auth: the first Firebase CLI that enables sign-in methods without adding a web app to a project that has the apps of flutterfire configure, which SMF reads from firebase --version. SMF does not update the Firebase CLI. For an older one, it tells which command it found, in which version, and how to update it.

The app compiles without any of them, so none of them stops the run. A missing one is a warning with instructions, and smf create --explain shows the state of each.

In a terminal, unless the run has --skip-external-setup, SMF offers to fix what it can, and asks before each step. Each question says what the check found and how to fix it by hand, then asks whether to set it up now.

It can install the Firebase CLI with npm, also when a firebase command is on the PATH but does not run, such as one that needs another Node.js. The question then says how firebase --version ended. When Node.js is missing or older than 20, it installs Node.js first: on macOS with nvm when the Node.js on the PATH is one of nvm, or else with Homebrew or nvm, with nvm on Linux, and with winget, Chocolatey, Scoop or the portable ZIP of Node.js on Windows. The installation can take minutes, and its progress shows what it is doing. When the directory of the Firebase CLI is not on the PATH of new terminals, the installation adds it to the profile of the shell or, on Windows, to the PATH of the user. On Linux it also moves the global directory of npm to ~/.npm-global, so that it needs no sudo, and adds a firebase command to ~/.local/bin that runs the Firebase CLI with the Node.js that installed it, whatever Node.js a terminal has, such as another default version of nvm. On macOS, it adds that command, and ~/.local/bin to the PATH of new terminals, when it installs the Firebase CLI with a Node.js of nvm. SMF lists these changes afterwards. When npm fails on macOS or Linux, SMF offers the standalone binary of the Firebase CLI instead, curl -sL https://firebase.tools | bash, which may ask for your password to write to /usr/local/bin.

It can log you in with firebase login, which asks its own questions in the terminal. firebase login waits for the browser to come back to a server on the machine, which a browser on another machine cannot reach. So when SSH_CONNECTION, SSH_CLIENT or SSH_TTY has a value, as in a session over SSH, SMF runs firebase login --no-localhost instead. That command gives a link to open on any device and asks for the authorization code shown there. On a remote machine where none of them has a value, log in yourself with firebase login --no-localhost.

A login of the Firebase CLI can expire or be revoked. firebase login:list still lists such an account, but the Firebase CLI can no longer reach your projects, so flutterfire finds no project and offers to create one. That is why SMF checks the login with firebase projects:list --debug, which only reads. The Firebase CLI reports the same error whether Google rejects the login or cannot be reached at all, and only its debug output tells the two apart. If Google rejects the login, SMF says that it has expired or is no longer valid and offers to log you in again with firebase login --reauth, or with firebase login --reauth --no-localhost over SSH. Plain firebase login would only say that you are logged in already. If the command cannot reach Google, as on a machine without a network, SMF says that it could not check the login and why, and leaves the configuration for later. The check needs the network and usually takes a few seconds; SMF shows the progress "Checking the machine" while the checks run. On a network that drops requests without answering, SMF gives up on the listing after 40 seconds and says that it could not check the login. Ctrl-C stops the check, along with the whole run.

It can activate flutterfire_cli 1.4.1 when no version is active or an older one is. The question names the version that is active and says that 1.4.1 takes its place:

FlutterFire CLI 1.4.1 or a later 1.x is needed by firebase_core, but flutterfire_cli 1.4.0 is active. Activate 1.4.1 in its place with "dart pub global activate flutterfire_cli 1.4.1". Set it up now?

SMF never replaces a newer major version, which other apps may need, and only tells you how to activate 1.4.1 in that case.

SMF cannot install the gem xcodeproj for you. Install it with gem install xcodeproj, with a Ruby version manager or, for the Ruby of macOS, with sudo.

note

On Windows, SMF installs with a PowerShell script. CI runs it on Windows with the Node.js of the machine, but the installation of Node.js by the script, with winget, Chocolatey, Scoop or the portable ZIP, has not run on Windows yet.

After generation: configure dev​

Once the app has its packages, SMF shows the command below and asks whether to run it now. It runs the command in the app with the terminal attached, so the FlutterFire CLI can ask you for the Firebase project:

dart tool/configure_firebase.dart dev

The script configures one flavor, here the one that a flutter command builds without --flavor. It runs flutterfire configure with the ids of that flavor. On macOS it runs it three times, once for each Xcode build configuration of the flavor, since flutterfire writes one build configuration in a run. Elsewhere it runs it once. flutterfire registers the Android and iOS apps of the flavor in the project and writes:

  • lib/firebase_options_dev.dart;
  • android/app/src/dev/google-services.json;
  • firebase.json;
  • on macOS, ios/flavors/dev/GoogleService-Info.plist and the changes to the Xcode project.

The script then formats the options and runs what follows each configuration.

SMF prints the command for later instead of running it in these cases:

  • The run has no terminal or has --no-input, and the FlutterFire CLI needs you to choose a project.
  • The run has --skip-external-setup.
  • A check that the command needs did not pass: the Firebase CLI, the login, the FlutterFire CLI or, on macOS, the gem xcodeproj. The command would fail, so SMF does not ask about it. SMF names these checks with the command, also when the run has no terminal or skips external setup, so you know what to set up before you run the command.
  • You answer no, or the command fails.

A command that you or the options of the run left for later is listed after the line that the app is created. A command that failed, or that only a check kept from running, is a warning. The machine and external setup shows both.

The app is complete either way. Run the command in its directory when you are ready.

In an app with firebase_auth, a second question follows once the script configured the flavor: whether to enable the sign-in methods in the project that you chose.

One project for each flavor​

Each flavor may have a Firebase project of its own, or share one with another flavor. smf create only configures dev. Configure the others when you need them, with one command for each flavor:

dart tool/configure_firebase.dart staging --project=my-app-staging
dart tool/configure_firebase.dart prod --project=my-app
Part of the commandWhat it does
The flavorRequired. The script stops without one, or with a name that is no flavor of the app.
--project=<id>The Firebase project. In a terminal you may leave it out, and flutterfire asks. Without a terminal, the script stops without it.
Other argumentsThey go to flutterfire as they are, such as --account=<email>.

Commit what the script writes: the options, android/app/src/<flavor>/google-services.json, firebase.json and ios/flavors/<flavor>/GoogleService-Info.plist. A build from a clone of the repository, such as one of CI, uses them.

Run the script again at any time. It changes nothing but the ids of the build phases that flutterfire added, or moves the flavor to another project. The ids of each flavor are at the top of the script, so if you change them in the Android and Xcode projects, change them there too.

Use this script rather than flutterfire configure. That command writes one configuration that every flavor then shares: ios/Runner/GoogleService-Info.plist goes into the resources of every flavor, and android/app/google-services.json is what the Google services plugin takes for any variant without a file of its own.

What repeats for each project​

A Firebase project knows each flavor as an app of its own, and what you set in one project is not in another. So the same has to be done for the other environments, if they need it. SMF does not hide this, because Firebase works this way with or without SMF. A flavor that shares a project with another needs only what goes with its own app.

WhatHow oftenHow
The apps and the optionsEach flavordart tool/configure_firebase.dart <flavor> --project=<id>
The sign-in methodsEach projectdart tool/enable_firebase_sign_in.dart --project <id>
What Google sign-in needsEach project, and each machine that builds for debuggingThe same script, for the apps that firebase.json names in that project
Google AnalyticsEach projectBy hand
The APNs keyEach Apple app that you send to. One key serves every app of its team.By hand
Apple as a sign-in method, with its key and Services IDEach projectBy hand
The SHA of the release keysEach Android app that such a key signsBy hand, or with the Firebase CLI
The App IDEach bundle id that you signBy hand

A flavor that is not configured​

WhereWhat happens
No flavor is configuredEvery build passes, and the app stops at start-up with an UnsupportedError that gives the command.
Android, once another flavor is configuredThe build of a flavor that is not fails with File google-services.json is missing.
iOS without CrashlyticsIt builds and stops at start-up with that UnsupportedError, unless a build phase that flutterfire adds for another Firebase package fails the build first.
iOS with Crashlytics, on macOS, once another flavor is configuredThe build fails in the phase of Crashlytics with FirebaseJsonException: Please run "flutterfire configure".

In each case, what it takes is dart tool/configure_firebase.dart <flavor> --project=<id>.

In Xcode, select the scheme of a flavor. It builds that flavor. The scheme Runner takes the flavor from ios/Flutter/Generated.xcconfig, which has none after a fresh flutter pub get, so appFlavor is null there and firebaseOptionsOfFlavor throws an UnsupportedError. flutter build ios --config-only --flavor <flavor> gives that file a flavor.

What follows each configuration​

After its last run of flutterfire, the script runs dart format on the options, which only warns when it fails, and then each Dart file of tool/firebase_configured/, in the order of their names. A module that depends on firebase_core puts a file there for what has to follow each run of flutterfire, as firebase_crashlytics does below. An app whose modules have nothing to run there has no such directory. You can add a file of your own too.

Crashlytics and flutter build ipa​

For an app with Crashlytics, the FlutterFire CLI adds a build phase to the Xcode project on macOS that uploads the debug symbols with the upload script of Crashlytics. Flutter keeps the Swift packages of the app, and the script with them, in build/ios/SourcePackages. The phase of flutterfire_cli 1.4.1 looks for the script in the build directory of Xcode, which flutter run and flutter build ios set to build/ios, but flutter build ipa does not, so the archive would fail in that phase.

flutterfire writes the phase again each time it runs, so firebase_crashlytics adds tool/firebase_configured/fix_crashlytics_phase.dart, and tool/configure_firebase.dart runs it after each flavor. On macOS it points the phase at build/ios/SourcePackages, changes nothing else in ios/Runner.xcodeproj/project.pbxproj, and leaves a project that it fixed already as it is. Elsewhere it does nothing.

After running flutterfire configure yourself on macOS, run it too:

dart tool/firebase_configured/fix_crashlytics_phase.dart

The phase looks up the build configuration in firebase.json, so a build of a flavor that is not configured fails there.

Google Analytics​

firebase_analytics records its events in Google Analytics, which a Firebase project that flutterfire configure creates does not have. Until Google Analytics is on in the project, the app logs its events, but they show up nowhere. Turning it on is a step by hand of every app with the module, once in each Firebase project of your flavors, and the click-by-click guide shows each screen. A project that has Google Analytics already needs nothing.

Sign-in methods of Firebase Authentication​

An app with firebase_auth needs one more thing than the configuration: the ways to sign in are enabled in the Firebase project, not in the code. That is Email/Password in every mode of the app, Anonymous when the mode of the app is anonymous, and Google and Apple for the ways to sign in of an app with google_sign_in or apple_sign_in. Until they are enabled, signing in and signing up fail with the reason notConfigured, and the developerHint of the failure tells how to enable them. In the mode anonymous, the app also starts without a user.

The module generates a script of the app that enables them, tool/enable_firebase_sign_in.dart. Right after dart tool/configure_firebase.dart dev configured the app, smf create asks whether to run it:

Enabling the sign-in methods of the app in its Firebase project (dart tool/enable_firebase_sign_in.dart), for firebase_auth. If the app signs in with Google, it also gives Google the app's name and the Firebase CLI account's email for its consent screen, registers the SHA-1 and SHA-256 of this machine's Android debug key (creating the key if there is none, as the first debug build would), downloads the app's Firebase files again and writes the iOS URL scheme into ios/Runner/Info.plist. Run it now?

A step cannot tell which ways to sign in the app has, so smf create asks the same question in every app with firebase_auth, also in one without Google.

SMF prints the command for later instead of asking in these cases:

  • tool/configure_firebase.dart did not run, or failed, or you left it for later. The script waits with it.
  • The run has no terminal, has --no-input or has --skip-external-setup.
  • The Firebase CLI of the machine is missing or older than 15.34.0.

To enable the methods later, or after you changed the mode of the app to anonymous, run the script in the directory of the app:

dart tool/enable_firebase_sign_in.dart

Each project needs the methods enabled, and a run covers one project. So once firebase.json has several, the script stops and lists them. Run it once for each:

dart tool/enable_firebase_sign_in.dart --project my-app-staging
dart tool/enable_firebase_sign_in.dart --project my-app
What the script does
The projectIt reads the id of the project from firebase.json, where tool/configure_firebase.dart wrote it, and says which project it enables the methods for. --project <id> names another. It stops when firebase.json has no project or several, unless you name one.
The modeIt reads the mode from authMode in lib/core/auth/app_session.dart, and enables Anonymous only in the mode anonymous. It stops when that file does not tell the mode.
The methodsIt enables them with firebase deploy --only auth of the Firebase CLI, which enables what its configuration names. It never disables a method.
The files of the appIt runs the Firebase CLI in a temporary directory, with a configuration of its own there, and removes the directory. firebase.json of the app stays as it is, so a firebase deploy of the app does not touch the sign-in methods. It changes no file of the app but those that Google needs, as below.
The accountIn that temporary directory, an account that firebase login:use chose for the directory of the app does not apply. --account <email> names the account.
Other argumentsThey go to the Firebase CLI as they are, such as --debug.
Ctrl+CThe script lets the Firebase CLI end, removes the temporary directory and exits with the code 130.

The Firebase CLI enables the methods through an app of the project. Before 15.34.0 that had to be a web app, and the Firebase CLI added one, named "Default Web App", to a project without one. 15.34.0 takes an app of any platform, and adds a web app only to a project without any app. The script runs after tool/configure_firebase.dart, which registers the Android and iOS apps, so the project has an app by then. The script asks the Firebase CLI for its version first, and stops with what to do when it is missing or older.

You can also enable the methods in the Firebase console: open Authentication > Sign-in method of each project that a flavor is configured for, and enable Email/Password, Anonymous for the mode anonymous, and Google and Apple for the ways to sign in of the app.

Google​

For a way to sign in of Google, the script does more than enable it:

  • It gives Google the name of the app and the email address of the account of the Firebase CLI for the consent screen that users see when they sign in. The address is the support email there: the one that --account names, or else the one that the Firebase CLI is logged in as. When the Firebase CLI runs as no user account, as with a service account, the script leaves Google out and says so. Run it with --account <email> then, or enable Google in the Firebase console.
  • It registers the SHA-1 and SHA-256 of the Android debug key of the machine that runs it for each of those Android apps, which Google uses to recognize the Android app, with firebase apps:android:sha:create, and only the hashes that the project does not list yet. When the machine has no debug key yet, the script creates it where the Android Gradle plugin keeps it, as the first debug build would. Every machine has a debug key of its own, so run the script on each machine that builds the app for debugging, or add the SHA of its key in the Firebase console, in the settings of the Android app.
  • It downloads the Firebase files of those apps again, such as android/app/src/dev/google-services.json and ios/flavors/dev/GoogleService-Info.plist, once they have the clients of Google, and writes them to the paths that firebase.json names.
  • It writes the URL scheme of each of those iOS apps, REVERSED_CLIENT_ID of its GoogleService-Info.plist, into CFBundleURLTypes of ios/Runner/Info.plist. The flavors share that file, so it lists a scheme for each iOS app that the script ran for, in the build of every flavor. It adds a scheme and removes none: after a flavor moves to another project, remove the scheme of the former project there by hand.

The script sets up the debug key, the files and the URL scheme only for the apps that firebase.json names in the project of the run, and a project that no flavor is configured for gets none of it. So after you configure a flavor for another project, run the script for that project too. A step that fails after the methods are enabled ends the script with the code 1, and what it did before stays done.

A release of the Android app is signed with another key, whose SHA-1 and SHA-256 Google needs too: those of the upload key, and of the app signing key of Google Play, under Test and release > App integrity in the Play Console. Add each with the Firebase CLI, for the Android app of each flavor that such a key signs, with the id of that app and of its project from firebase.json:

firebase apps:android:sha:create <appId> <sha> --project=<id>

This is a step by hand of the app, and the click-by-click guide shows where the Play Console has both keys and where the Firebase console takes them.

Apple​

No command of the Firebase CLI enables Apple, so in an app with Sign in with Apple the script tells what to set in the Firebase console: add Apple under Authentication > Sign-in method.

Deleting an account that signs in with Apple revokes the tokens that Apple gave the app, as the App Store asks of an app with Sign in with Apple (App Store Review Guideline 5.1.1(v)). Firebase does that with the Services ID and the OAuth code flow configuration, which you enter in the settings of the Apple provider there: your Apple team ID, and the Key ID and the private key of a key for Sign in with Apple. The private key is a secret: enter it in the console yourself, and never put it into the app or its repository. Until they are set, deleting an account that signs in with Apple fails with notConfigured, and the account stays.

Both are steps by hand of the app, and the click-by-click guides of the provider and of the key and the Services ID show each screen, from the key and the Services ID in your Apple Developer account to the settings of the Apple provider.

The ways to sign in are a setting of the project. A project that you configure the app for later needs them enabled too.

Firebase sends the message with which a user sets a new password from a template of the project, under Authentication > Templates. In an app with languages, the app asks Firebase for the message in the language that it is in.

Cloud Messaging​

An app with firebase_messaging receives push messages through Firebase Cloud Messaging. Firebase sends messages to iOS through the Apple Push Notification service (APNs), with an APNs key of your team. You upload the key by hand:

  1. In your Apple Developer account, under Certificates, Identifiers & Profiles, create a key under Keys with the Apple Push Notifications service (APNs) enabled. Download its .p8 file, and note its Key ID and your Team ID.
  2. In the Firebase console, open the settings of the project, then Cloud Messaging, and upload the key under the configuration of the Apple app, with its Key ID and your Team ID.

The Firebase console has this configuration under each Apple app of the project, so check it for the iOS app of each flavor that you send to, and repeat the upload where it has no key. One key serves every app of its team, so the file and the ids stay the same.

A key sends only to the apps of its team, so register the bundle id of each flavor that you send to as an App ID in that team, with Push Notifications. The section "Flavors" of the README of the app lists the bundle ids. The key matters only for sending: the iOS app gets its address without it, but Firebase cannot send to that address.

The iOS app is also signed with the entitlement aps-environment, which the push role adds, and a free personal team cannot sign it; see Push messages.

The upload of the key and the App ID are steps by hand of the app, and the click-by-click guides of the key and of the App ID show each screen.

A test message​

To send a device a test message, run the app in debug mode and copy the address that it prints:

The address of this device for push messages: <the address>

In the Firebase console, open Messaging, create a notification campaign, choose Send test message and paste the address.

A server sends a message with the HTTP v1 API of Firebase Cloud Messaging: POST https://fcm.googleapis.com/v1/projects/<project-id>/messages:send, with an OAuth token of a service account of the project of the flavor and the address as the token of the message:

{
"message": {
"token": "<the address of the device>",
"notification": {"title": "Hello", "body": "A message from the backend"},
"data": {"screen": "tasks"}
}
}

A message with a notification shows on the device once the user has allowed the notifications of the app, and its data reaches the app as the data of a PushMessage; see Notifications and push messages.

Configure again, or on another machine​

To configure a flavor for another Firebase project, or to set it up on another machine, run in the directory of the app:

dart pub global activate flutterfire_cli 1.4.1
firebase login
dart tool/configure_firebase.dart staging --project=<id>

Skip the activation if dart pub global list shows flutterfire_cli 1.4.1 or a later 1.x already. If firebase login says that you are logged in already but flutterfire finds no Firebase project, your login has probably expired. Run firebase login --reauth. With firebase_auth, then run dart tool/enable_firebase_sign_in.dart --project <id> for the new project.

The script changes the Xcode project only on macOS, and the build phases that flutterfire adds there, such as the upload of the debug symbols of Crashlytics, run flutterfire from ~/.pub-cache/bin. So every Mac that builds the app for iOS needs the FlutterFire CLI activated globally.

An app configured with flutterfire_cli 1.4.0​

The phase for Crashlytics that flutterfire_cli 1.4.0 adds does not find the upload script where Flutter puts the Swift packages, so flutter run and flutter build ios fail in that phase, even after you activate 1.4.1. Activate 1.4.1 and run tool/configure_firebase.dart again on a Mac for each configured flavor, and the new phase replaces the old one.

Linux and Windows​

On a system other than macOS, tool/configure_firebase.dart runs flutterfire once, which registers the iOS app and writes its options, but writes no GoogleService-Info.plist and leaves the Xcode project as it is, without the build phases it adds for some Firebase packages. The script says so when it ends. Run it again for that flavor on a Mac before building the iOS app there. SMF warns about this before it generates the app.

In CI​

A run without a terminal cannot choose a Firebase project, so in CI smf create generates the app with the placeholder options and prints the command for later, and after it the command that enables the sign-in methods of an app with firebase_auth. Such a run installs nothing either. Add --skip-external-setup anyway, as the command in scripts and CI does. It also holds back the steps of other modules that need an external service but ask nothing, which a run without a terminal would otherwise start.

tool/configure_firebase.dart itself works without a terminal once it has --project. The CI of an app needs no flutterfire on Android: it builds from the files that you committed.