The Flutter Kit logoThe Flutter Kit
Tutorial

Firebase for Flutter: The Complete Setup Guide in 2026

Firebase is the most popular backend for Flutter apps — and for good reason. This guide covers everything from initial FlutterFire CLI setup to Firestore CRUD, Cloud Storage, Cloud Functions, and security rules. Every step includes Dart code you can copy directly into your project.

Ahmed GaganAhmed Gagan
18 min read

Answer: Why Firebase for Flutter?

Firebase is the default backend choice for Flutter developers in 2026 because it offers a generous free tier, tight SDK integration maintained by Google, real-time database sync, authentication, file storage, serverless functions, and push notifications — all from a single dashboard. The FlutterFire CLI makes initial setup a one-command operation, and the ecosystem of plugins covers virtually every Firebase service.

Prerequisites

Before we start, make sure you have:

  • Flutter 3.24+ installed and working (flutter doctor passes)
  • A Firebase account (free tier is sufficient)
  • Node.js 18+ installed (required for FlutterFire CLI and Cloud Functions)
  • A Flutter project created (flutter create my_app)

Step 1: FlutterFire CLI Setup

The FlutterFire CLI automates the tedious process of connecting your Flutter project to Firebase. It generates platform-specific configuration files and a Dart options file — no more manually downloading google-services.json and GoogleService-Info.plist.

Install the CLI

# Install the Firebase CLI (if not already installed)
npm install -g firebase-tools

# Log in to Firebase
firebase login

# Install the FlutterFire CLI
dart pub global activate flutterfire_cli

Configure Your Project

# Run from your Flutter project root
flutterfire configure

The CLI will ask you to select a Firebase project (or create one), choose which platforms to configure (iOS, Android, Web, macOS), and it will automatically:

  • Register your app in the Firebase console for each platform
  • Download and place the platform config files in the correct locations
  • Generate lib/firebase_options.dart with all platform configurations

Initialize Firebase in Your App

Add the firebase_core package and initialize Firebase before your app starts:

// pubspec.yaml
dependencies:
  firebase_core: ^3.8.0

// main.dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  runApp(const MyApp());
}

That is it for setup. Firebase is now connected to your Flutter app on all configured platforms. Every subsequent Firebase service you add will use this initialization.

Step 2: Firestore — Your Real-Time Database

Cloud Firestore is a NoSQL document database that syncs data in real-time across all connected clients. It is the most commonly used Firebase service in Flutter apps. Add it to your project:

// pubspec.yaml
dependencies:
  cloud_firestore: ^5.6.0

Data Model

Firestore organizes data in documents and collections. Here is a typical Dart model with JSON serialization:

class Note {
  final String id;
  final String title;
  final String content;
  final DateTime createdAt;
  final String userId;

  Note({
    required this.id,
    required this.title,
    required this.content,
    required this.createdAt,
    required this.userId,
  });

  factory Note.fromFirestore(DocumentSnapshot doc) {
    final data = doc.data() as Map<String, dynamic>;
    return Note(
      id: doc.id,
      title: data['title'] ?? '',
      content: data['content'] ?? '',
      createdAt: (data['createdAt'] as Timestamp).toDate(),
      userId: data['userId'] ?? '',
    );
  }

  Map<String, dynamic> toFirestore() {
    return {
      'title': title,
      'content': content,
      'createdAt': Timestamp.fromDate(createdAt),
      'userId': userId,
    };
  }
}

Create (Add a Document)

final firestore = FirebaseFirestore.instance;

// Add with auto-generated ID
Future<String> addNote(Note note) async {
  final docRef = await firestore
      .collection('users')
      .doc(note.userId)
      .collection('notes')
      .add(note.toFirestore());
  return docRef.id;
}

// Add with custom ID
Future<void> setNote(Note note) async {
  await firestore
      .collection('users')
      .doc(note.userId)
      .collection('notes')
      .doc(note.id)
      .set(note.toFirestore());
}

Read (Query Documents)

// Get a single document
Future<Note?> getNote(String userId, String noteId) async {
  final doc = await firestore
      .collection('users')
      .doc(userId)
      .collection('notes')
      .doc(noteId)
      .get();

  if (!doc.exists) return null;
  return Note.fromFirestore(doc);
}

// Get all notes for a user, ordered by creation date
Future<List<Note>> getNotes(String userId) async {
  final snapshot = await firestore
      .collection('users')
      .doc(userId)
      .collection('notes')
      .orderBy('createdAt', descending: true)
      .get();

  return snapshot.docs
      .map((doc) => Note.fromFirestore(doc))
      .toList();
}

// Real-time stream of notes (updates automatically)
Stream<List<Note>> watchNotes(String userId) {
  return firestore
      .collection('users')
      .doc(userId)
      .collection('notes')
      .orderBy('createdAt', descending: true)
      .snapshots()
      .map((snapshot) => snapshot.docs
          .map((doc) => Note.fromFirestore(doc))
          .toList());
}

Update

Future<void> updateNote(
  String userId,
  String noteId,
  Map<String, dynamic> updates,
) async {
  await firestore
      .collection('users')
      .doc(userId)
      .collection('notes')
      .doc(noteId)
      .update(updates);
}

// Example: Update just the title
await updateNote(userId, noteId, {
  'title': 'Updated Title',
});

Delete

Future<void> deleteNote(String userId, String noteId) async {
  await firestore
      .collection('users')
      .doc(userId)
      .collection('notes')
      .doc(noteId)
      .delete();
}

Using Streams in Your UI

The real power of Firestore in Flutter is real-time updates. Combine streams with StreamBuilder or a BLoC pattern:

class NotesScreen extends StatelessWidget {
  final String userId;
  const NotesScreen({super.key, required this.userId});

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<List<Note>>(
      stream: watchNotes(userId),
      builder: (context, snapshot) {
        if (snapshot.connectionState ==
            ConnectionState.waiting) {
          return const Center(
            child: CircularProgressIndicator(),
          );
        }

        if (snapshot.hasError) {
          return Center(
            child: Text('Error: ${snapshot.error}'),
          );
        }

        final notes = snapshot.data ?? [];

        if (notes.isEmpty) {
          return const Center(
            child: Text('No notes yet. Create one!'),
          );
        }

        return ListView.builder(
          itemCount: notes.length,
          itemBuilder: (context, index) {
            final note = notes[index];
            return ListTile(
              title: Text(note.title),
              subtitle: Text(note.content),
              trailing: Text(
                '${note.createdAt.month}/${note.createdAt.day}',
              ),
            );
          },
        );
      },
    );
  }
}

Step 3: Firebase Storage — File Uploads

Firebase Cloud Storage lets you upload and serve user-generated content — profile pictures, documents, images, and more. It is backed by Google Cloud Storage and includes a CDN for fast delivery.

// pubspec.yaml
dependencies:
  firebase_storage: ^12.4.0

Upload a File

import 'dart:io';
import 'package:firebase_storage/firebase_storage.dart';

class StorageService {
  final _storage = FirebaseStorage.instance;

  /// Upload a file and return the download URL
  Future<String> uploadFile({
    required File file,
    required String userId,
    required String fileName,
  }) async {
    final ref = _storage
        .ref()
        .child('users')
        .child(userId)
        .child(fileName);

    // Upload with metadata
    final metadata = SettableMetadata(
      contentType: 'image/jpeg',
      customMetadata: {'uploadedBy': userId},
    );

    final uploadTask = ref.putFile(file, metadata);

    // Optional: Track upload progress
    uploadTask.snapshotEvents.listen((event) {
      final progress =
          event.bytesTransferred / event.totalBytes;
      debugPrint(
        'Upload progress: ${(progress * 100).toStringAsFixed(0)}%'
      );
    });

    // Wait for completion and get download URL
    await uploadTask;
    return await ref.getDownloadURL();
  }

  /// Upload from memory (e.g., compressed image bytes)
  Future<String> uploadBytes({
    required Uint8List bytes,
    required String path,
  }) async {
    final ref = _storage.ref().child(path);
    await ref.putData(bytes);
    return await ref.getDownloadURL();
  }

  /// Delete a file
  Future<void> deleteFile(String path) async {
    await _storage.ref().child(path).delete();
  }
}

Profile Picture Upload Example

import 'package:image_picker/image_picker.dart';

Future<void> uploadProfilePicture(String userId) async {
  final picker = ImagePicker();
  final image = await picker.pickImage(
    source: ImageSource.gallery,
    maxWidth: 512,
    maxHeight: 512,
    imageQuality: 80,
  );

  if (image == null) return;

  final file = File(image.path);
  final storageService = StorageService();

  final downloadUrl = await storageService.uploadFile(
    file: file,
    userId: userId,
    fileName: 'profile.jpg',
  );

  // Save the URL to Firestore
  await FirebaseFirestore.instance
      .collection('users')
      .doc(userId)
      .update({'avatarUrl': downloadUrl});
}

Step 4: Cloud Functions — Server-Side Logic

Cloud Functions let you run backend code without managing servers. Common use cases include: processing payments, sending notifications, generating thumbnails, validating data, and proxying third-party API calls (like OpenAI). Functions are written in TypeScript or Python and deployed to Google's infrastructure.

Initialize Cloud Functions

# From your project root
firebase init functions

# Choose TypeScript (recommended)
# This creates a functions/ directory

Write a Function

// functions/src/index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

admin.initializeApp();

// HTTP callable function — called from Flutter
export const createUserProfile = functions
  .https.onCall(async (data, context) => {
    // Verify authentication
    if (!context.auth) {
      throw new functions.https.HttpsError(
        'unauthenticated',
        'Must be logged in to create a profile.'
      );
    }

    const uid = context.auth.uid;
    const { displayName, email } = data;

    await admin.firestore()
      .collection('users')
      .doc(uid)
      .set({
        displayName,
        email,
        createdAt: admin.firestore.FieldValue
          .serverTimestamp(),
        isPro: false,
      });

    return { success: true };
  });

// Firestore trigger — runs when a document is created
export const onNoteCreated = functions
  .firestore
  .document('users/{userId}/notes/{noteId}')
  .onCreate(async (snapshot, context) => {
    const { userId } = context.params;

    // Increment the user's note count
    await admin.firestore()
      .collection('users')
      .doc(userId)
      .update({
        noteCount: admin.firestore.FieldValue
          .increment(1),
      });
  });

Call Functions from Flutter

// pubspec.yaml
dependencies:
  cloud_functions: ^5.2.0

// In your Dart code
import 'package:cloud_functions/cloud_functions.dart';

class FunctionsService {
  final _functions = FirebaseFunctions.instance;

  Future<void> createUserProfile({
    required String displayName,
    required String email,
  }) async {
    final callable = _functions
        .httpsCallable('createUserProfile');

    try {
      final result = await callable.call({
        'displayName': displayName,
        'email': email,
      });
      debugPrint('Profile created: ${result.data}');
    } on FirebaseFunctionsException catch (e) {
      debugPrint('Function error: ${e.code} - ${e.message}');
      rethrow;
    }
  }
}

Deploy Functions

# Deploy all functions
firebase deploy --only functions

# Deploy a specific function
firebase deploy --only functions:createUserProfile

Step 5: Firestore Security Rules

Security rules are critical. Without them, anyone can read and write to your entire database. Firestore rules control access at the document level based on authentication state, user identity, and data validation.

Basic Rules Structure

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // User profiles: only the owner can read/write
    match /users/{userId} {
      allow read: if request.auth != null
        && request.auth.uid == userId;
      allow create: if request.auth != null
        && request.auth.uid == userId;
      allow update: if request.auth != null
        && request.auth.uid == userId;
      allow delete: if false; // Prevent deletion

      // User's notes subcollection
      match /notes/{noteId} {
        allow read: if request.auth != null
          && request.auth.uid == userId;
        allow create: if request.auth != null
          && request.auth.uid == userId
          && request.resource.data.title is string
          && request.resource.data.title.size() > 0
          && request.resource.data.title.size() <= 200;
        allow update: if request.auth != null
          && request.auth.uid == userId;
        allow delete: if request.auth != null
          && request.auth.uid == userId;
      }
    }

    // Public data — anyone can read, no one can write
    match /public/{docId} {
      allow read: if true;
      allow write: if false;
    }

    // Default deny everything else
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

Storage Security Rules

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {

    // Users can only upload to their own folder
    match /users/{userId}/{allPaths=**} {
      allow read: if request.auth != null;
      allow write: if request.auth != null
        && request.auth.uid == userId
        && request.resource.size < 5 * 1024 * 1024
        && request.resource.contentType
            .matches('image/.*');
    }

    // Deny everything else
    match /{allPaths=**} {
      allow read, write: if false;
    }
  }
}

Deploy Rules

# Deploy Firestore rules
firebase deploy --only firestore:rules

# Deploy Storage rules
firebase deploy --only storage

Step 6: Offline Support and Performance

Firestore in Flutter has built-in offline persistence enabled by default on mobile. This means your app works without an internet connection — reads are served from the local cache, and writes are queued and synced when connectivity returns.

Configure Offline Persistence

// Firestore offline settings
FirebaseFirestore.instance.settings = const Settings(
  persistenceEnabled: true,
  cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);

Performance Tips

  • Limit query results. Always use .limit() on queries that could return many documents. Fetching 1,000 documents when you only display 20 wastes bandwidth and memory.
  • Use pagination. For lists, implement cursor-based pagination with .startAfterDocument() rather than loading everything at once.
  • Index compound queries. Firestore requires composite indexes for queries with multiple where clauses or a where + orderBy combination. The error message will include a direct link to create the required index.
  • Batch writes. If you need to update multiple documents atomically, use WriteBatch instead of individual writes.

Batch Writes Example

Future<void> deleteAllNotes(String userId) async {
  final batch = FirebaseFirestore.instance.batch();

  final snapshot = await FirebaseFirestore.instance
      .collection('users')
      .doc(userId)
      .collection('notes')
      .get();

  for (final doc in snapshot.docs) {
    batch.delete(doc.reference);
  }

  await batch.commit();
}

Step 7: Push Notifications with Firebase Messaging

Firebase Cloud Messaging (FCM) is the standard for push notifications on both iOS and Android. It is free with no message limits.

// pubspec.yaml
dependencies:
  firebase_messaging: ^15.2.0

// Initialize and request permissions
class NotificationService {
  final _messaging = FirebaseMessaging.instance;

  Future<void> initialize() async {
    // Request permission (required on iOS)
    final settings = await _messaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    if (settings.authorizationStatus ==
        AuthorizationStatus.authorized) {
      // Get the FCM token
      final token = await _messaging.getToken();
      debugPrint('FCM Token: $token');

      // Save token to Firestore for server-side sends
      if (token != null) {
        await _saveTokenToFirestore(token);
      }

      // Listen for token refresh
      _messaging.onTokenRefresh.listen(
        _saveTokenToFirestore,
      );

      // Handle foreground messages
      FirebaseMessaging.onMessage.listen((message) {
        debugPrint(
          'Foreground message: ${message.notification?.title}'
        );
        // Show a local notification or in-app alert
      });
    }
  }

  Future<void> _saveTokenToFirestore(
    String token,
  ) async {
    final user = FirebaseAuth.instance.currentUser;
    if (user != null) {
      await FirebaseFirestore.instance
          .collection('users')
          .doc(user.uid)
          .update({'fcmToken': token});
    }
  }
}

Project Structure Best Practices

Here is how I recommend organizing Firebase-related code in a Flutter project. This follows the repository pattern that The Flutter Kit uses:

lib/
  data/
    repositories/
      auth_repository.dart       # FirebaseAuth wrapper
      notes_repository.dart      # Firestore CRUD for notes
      storage_repository.dart    # Firebase Storage wrapper
      functions_repository.dart  # Cloud Functions caller
    models/
      note.dart                  # Data model with Firestore serialization
      user_profile.dart          # User profile model
  services/
    notification_service.dart    # FCM setup and handling
  firebase_options.dart          # Generated by FlutterFire CLI

Keep Firebase imports out of your UI layer. Your widgets should depend on repositories, not directly on Firestore or Firebase Auth. This makes testing easier and lets you swap backends without rewriting your UI.

Common Pitfalls and How to Avoid Them

  1. Forgetting security rules. The default test-mode rules expire after 30 days. Always write proper rules before going to production. An unprotected Firestore database is a data breach waiting to happen.
  2. Not handling offline state. Firestore's offline cache is great, but your UI should indicate when data might be stale. Check snapshot.metadata.isFromCache to show an offline indicator.
  3. Ignoring Firestore read costs. Every document read counts toward your quota. A feed that loads 50 items with 3 subcollection reads each is 150 reads per screen load. Use denormalization strategically to reduce read counts.
  4. Not cleaning up stream subscriptions. If you use .snapshots() streams in a BLoC or ViewModel, make sure to cancel them in dispose(). Leaked streams cause memory leaks and unnecessary Firestore reads.
  5. Skipping composite indexes. When your query fails with an index error, click the link in the error message to create the required index automatically. Do not try to restructure your query to avoid indexes — they are part of Firestore's design.

Skip the Setup — Use The Flutter Kit

Everything covered in this tutorial — FlutterFire configuration, Firestore repositories, Storage service, Cloud Functions, security rules, push notifications, and the repository pattern — comes pre-built in The Flutter Kit. You run the setup CLI, paste your Firebase project ID, and the entire backend stack is configured for both iOS and Android.

The kit also includes Firebase Authentication with Sign in with Apple and Google Sign-In, RevenueCat subscriptions, OpenAI integration via Cloud Functions, and three onboarding templates — all wired together and production-ready.

Share this article

Ready to ship your Flutter app faster?

The Flutter Kit gives you a production-ready Flutter codebase with onboarding, paywalls, auth, AI integrations, and more. Stop building boilerplate. Start building your product.

Get The Flutter Kit