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 doctorpasses) - 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_cliConfigure Your Project
# Run from your Flutter project root
flutterfire configureThe 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.dartwith 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.0Data 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.0Upload 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/ directoryWrite 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:createUserProfileStep 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 storageStep 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
whereclauses or awhere+orderBycombination. The error message will include a direct link to create the required index. - Batch writes. If you need to update multiple documents atomically, use
WriteBatchinstead 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 CLIKeep 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
- 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.
- Not handling offline state. Firestore's offline cache is great, but your UI should indicate when data might be stale. Check
snapshot.metadata.isFromCacheto show an offline indicator. - 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.
- Not cleaning up stream subscriptions. If you use
.snapshots()streams in a BLoC or ViewModel, make sure to cancel them indispose(). Leaked streams cause memory leaks and unnecessary Firestore reads. - 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.