Firebase & Backend Services
Irbisa · cheatsheetSeptember 13, 2026

Firebase & Backend Services

FlutterFire setup, Auth, Firestore, FCM, Remote Config, Crashlytics, App Check, Supabase

Middle Developer16 itemscompressed for a skim
  1. 01

    Your staging build writes to the production Firestore even though you swapped firebase_options.dart. What is still pointing at prod?

    Easy

    flutterfire configure generates three files per project — lib/firebase_options.dart, android/app/google-services.json and ios/Runner/GoogleService-Info.plist — and swapping only the Dart one leaves the native SDKs on the old project.

    // One run per Firebase project:
    //
    //   flutterfire configure --project=myapp-dev \
    //     --out=lib/firebase/options_dev.dart \
    //     --android-package-name=com.myapp.dev --ios-bundle-id=com.myapp.dev \
    //     --android-out=android/app/src/dev/google-services.json \
    …
  2. 02

    At cold start the app shows the login screen for a frame and then jumps to the feed. What is authStateChanges() actually doing?

    Easy

    authStateChanges() emits a User? only once the SDK has restored the persisted session from disk, so your first frame ran while the answer was still unknown and you rendered null as "signed out".

    // ── Three states, not two ──
    class AuthGate extends StatelessWidget {
      const AuthGate({super.key});
    
      @override
      Widget build(BuildContext context) {
    …
  3. 03

    Adding Firestore, Storage and Analytics pushed cold start past two seconds. Which Firebase work can you move off the startup path?

    Easy

    Only Firebase.initializeApp() has to finish before the first frame; everything else is either lazy already or work you chose to await in main().

    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
    
      // The only await the first frame actually needs.
      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
    …
  4. 04

    A guest played for a week, then signed in with Google, and everything they had saved is gone. What did that sign-in do?

    Medium

    signInWithCredential signed them into a different account: the anonymous session had its own uid, Google sign-in minted a new one, and every document keyed by the old uid is now orphaned.

    Future<void> upgradeGuestToGoogle() async {
      final auth = FirebaseAuth.instance;
      final guest = auth.currentUser!;
      final credential = await googleCredential();
    
      try {
    …
  5. 05

    The feed must show the three newest comments under every post. How does that requirement change your Firestore model?

    Medium

    Firestore has no joins, so a screen that must render in one round trip needs a document shaped for that screen: the three comments get denormalised onto the post.

    // N+1: one query, then a read per post. 30 posts = 31 round trips.
    Future<List<Post>> slowFeed() async {
      final posts = await db.collection('posts')
          .orderBy('createdAt', descending: true).limit(30).get();
      return Future.wait(posts.docs.map((p) async {
        final comments = await p.reference.collection('comments')
    …
  6. 06

    A message you just sent shows up in the list instantly, then jumps to a different position a second later. What are the snapshots telling you?

    Medium

    You are watching two events for one write: the local optimistic one, with hasPendingWrites true and a null server timestamp, then the server's, carrying the real timestamp that re-sorts the row.

    class _ChatState extends State<Chat> {
      // Built once. snapshots() inside build() re-subscribes on every rebuild.
      late final _messages = db
          .collection('rooms/${widget.roomId}/messages')
          .orderBy('createdAt', descending: true)
          .limit(50)
    …
  7. 07

    In airplane mode your await docRef.set(...) never returns, yet the new row is already on screen. Is that a bug?

    Medium

    No — that Future completes only when the server acknowledges the write, while the local cache applied it immediately, so awaiting a Firestore write in the UI is the bug.

    // Set before any other Firestore call. Mobile defaults: on, 100 MB.
    FirebaseFirestore.instance.settings = const Settings(
      persistenceEnabled: true,
      cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
    );
    …
  8. 08

    Your app already hides the Delete button from non-authors, so why must the Firestore rule check the author too, and what should permission-denied look like to the user?

    Medium

    Because the SDK talks straight to Firestore over the network: the button is UX, and the rule is the only thing between a stranger holding your app id and every document in the project.

    // firestore.rules — the only place ownership is actually enforced:
    //
    //   match /notes/{noteId} {
    //     allow read:   if resource.data.ownerId == request.auth.uid;
    //     allow create: if request.auth.uid == request.resource.data.ownerId
    //                   && request.resource.data.text is string;
    …
  9. 09

    Push notifications show up fine when the app is closed, but nothing appears while the user has it open. What is firebase_messaging doing?

    Medium

    A notification payload is drawn by the operating system, and the OS deliberately refuses to draw it while your app is in the foreground — a foreground message arrives on onMessage as plain data for you to display yourself.

    // Top level, not a closure, not a method — and the annotation is load-bearing.
    @pragma('vm:entry-point')
    Future<void> _onBackgroundMessage(RemoteMessage message) async {
      // A separate isolate: no providers, no navigator, no state from the app.
      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
      await PendingInbox.append(message.data['id']!); // leave a trace on disk
    …
  10. 10

    Crashlytics is in the app, users report crashes every day, and the dashboard is empty. What is missing from the wiring?

    Medium

    Adding the plugin catches native crashes only; Dart errors reach Crashlytics only if you hand them over through FlutterError.onError and PlatformDispatcher.instance.onError.

    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
    
      final crashlytics = FirebaseCrashlytics.instance;
      await crashlytics.setCrashlyticsCollectionEnabled(!kDebugMode);
    …
  11. 11

    The splash screen hangs for eight seconds on a weak connection because startup awaits Remote Config. How should that be wired instead?

    Medium

    Never await a network fetch before the first frame: set defaults, activate whatever was already fetched on a previous run, render, and let the new payload arrive in the background.

    final rc = FirebaseRemoteConfig.instance;
    
    Future<void> _bootstrapConfig() async {
      await rc.setConfigSettings(RemoteConfigSettings(
        fetchTimeout: const Duration(seconds: 8), // default is 60
        minimumFetchInterval:
    …
  12. 12

    Your callable Cloud Function works against the emulator and returns 'internal' with no message in production. What is going on?

    Medium

    A callable only forwards the code and message of an HttpsError; every other exception the function throws is flattened to internal with all details stripped, deliberately, so a stack trace never leaks to a public client.

    // Deployed to europe-west1 — the client would otherwise call us-central1.
    final functions = FirebaseFunctions.instanceFor(region: 'europe-west1');
    
    Future<OrderId> createOrder(Cart cart, {required String idempotencyKey}) async {
      final callable = functions.httpsCallable(
        'createOrder',
    …
  13. 13

    The Firestore bill jumped to five figures in a month while the user count stayed flat. Where do you look in the Flutter code first?

    Hard

    Firestore bills per document read, so the first suspect is always a listener that re-downloads its whole result set — and the classic cause is a snapshots() call written inside build().

    // A new stream object on every rebuild: the listener re-attaches and re-reads
    // every matching document. A parent setState costs a whole collection.
    @override
    Widget build(BuildContext context) => StreamBuilder<QuerySnapshot>(
          stream: FirebaseFirestore.instance
              .collection('orders')
    …
  14. 14

    Anyone can pull the API key out of your APK and call Firestore directly. What does App Check add, and what does it still not protect?

    Hard

    App Check attests that a request came from your genuine, unmodified binary on a genuine device — it answers "is this my app", never "may this user do this", which remains the job of security rules.

    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
    
      // Activate before the first Firestore / Storage / Functions call.
      await FirebaseAppCheck.instance.activate(
    …
  15. 15

    How do you test a Firebase-backed Flutter app so that no test run touches production and no widget test needs a network?

    Hard

    Three layers: a repository seam so widget tests never see Firebase at all, the Emulator Suite for code that genuinely talks to Firestore, and the JavaScript rules-testing library for the rules themselves.

    // 1. Widget tests never see Firebase — the seam is a repository interface.
    abstract interface class OrderRepository {
      Stream<List<Order>> watch(String uid);
    }
    
    class FakeOrders implements OrderRepository {
    …
  16. 16

    A client asks whether to build on Firebase, on Supabase, or on your own backend. How do you decide, and how do you keep the door open?

    Hard

    Choose on the shape of your reads and your compliance constraints rather than on the feature list, then spend the extra day that keeps the vendor SDK out of your widgets — that day is the whole exit path.

    // Domain first: no vendor types above this line, ever.
    class Order {
      const Order({required this.id, required this.total, required this.createdAt});
      final String id;
      final Money total;
      final DateTime createdAt; // not a Firestore Timestamp
    …