You have a working Flutter app, but user data stays on the device? Every login is a blank screen, and notifications never arrive? That means you are missing 90% of your app's potential. A backend is not optional: it is the difference between a prototype and a product that drives engagement, retention, and revenue.
We, at Meteora Web, have built dozens of apps with Flutter and Firebase for clients across Italy. We always start with the same question: how much does this infrastructure cost, and how much does it return? Firebase offers a generous free Spark Plan covering 50,000 reads and 20,000 writes per day on Firestore, 10,000 authentications per month, and unlimited push notifications. For a small business starting out, that's enough. Above that threshold, you pay only for what you use — no fixed lifetime fees.
In this guide you will see how to integrate Firebase Authentication, Firestore, and Cloud Messaging into Flutter with working code, real security rules, and logic that holds up under production traffic. No abstract theory: only what you need to go from zero to MVP.
Why use Firebase with Flutter for authentication and database?
Firebase is a Backend-as-a-Service (BaaS) platform from Google. You don't have to manage servers, scale manually, or worry about downtime. Flutter is the cross-platform framework. Put them together and you get an iOS and Android app with a ready backend in hours.
The concrete advantage? Your team doesn't waste time on infrastructure. You can focus on business logic, UX, and — for those of us who come from accounting — on costs and margins. Firestore charges based on usage, not a flat fee. If the app doesn't take off, you haven't spent thousands on unused servers.
Be careful of a common mistake: many developers skip Firestore security rules because "it's just a prototype." We see it all the time: databases open to everyone, exposed data, costs spiraling out of control. We set rules from the start, not after. It costs less.
How to configure Firebase in a Flutter project
Start from the Firebase Console (console.firebase.google.com). Create a project, add Android and iOS apps, and download the configuration files (google-services.json for Android, GoogleService-Info.plist for iOS).
Sponsored Protocol
In pubspec.yaml, add the dependencies:
dependencies:
flutter:
sdk: flutter
firebase_core: ^3.6.0
firebase_auth: ^5.1.0
cloud_firestore: ^5.4.0
firebase_messaging: ^15.1.0
flutter_local_notifications: ^18.0.0Then, in main.dart, initialize Firebase before anything else:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
runApp(const MyApp());
}If you don't want to use DefaultFirebaseOptions (which requires the flutterfire_cli package), you can pass credentials manually. We recommend using the CLI to simplify: flutterfire configure.
How to implement Firebase Authentication in Flutter?
Firebase Auth supports email/password, Google, Apple, Facebook, and more. For a small business app, email/password + Google is the minimum. Here is an example of registration and login.
Registration with email and password
Future<UserCredential> registerWithEmail(String email, String password) async {
try {
UserCredential userCredential = await FirebaseAuth.instance
.createUserWithEmailAndPassword(email: email, password: password);
return userCredential;
} on FirebaseAuthException catch (e) {
// Handle errors: email already in use, weak password, etc.
throw Exception(e.message);
}
}Login with email and password
Future<UserCredential> signInWithEmail(String email, String password) async {
try {
UserCredential userCredential = await FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password);
return userCredential;
} on FirebaseAuthException catch (e) {
throw Exception(e.message);
}
}A practical tip: right after registration, create a user document in Firestore with basic info and a timestamp. It will help with profiling and notifications.
Future<void> createUserDocument(User user) async {
await FirebaseFirestore.instance.collection('users').doc(user.uid).set({
'email': user.email,
'createdAt': FieldValue.serverTimestamp(),
'roles': ['user'],
});
}Google Sign-In
For Google, use the google_sign_in package:
Sponsored Protocol
Future<UserCredential> signInWithGoogle() async {
final GoogleSignInAccount? googleUser = await GoogleSignIn().signIn();
final GoogleSignInAuthentication? googleAuth = await googleUser?.authentication;
final credential = GoogleAuthProvider.credential(
accessToken: googleAuth?.accessToken,
idToken: googleAuth?.idToken,
);
return await FirebaseAuth.instance.signInWithCredential(credential);
}Remember to enable Google Sign-In in the Firebase Console (Authentication > Sign-in method) and configure SHA-1 in your Android project.
How to manage Firestore with security rules and efficient queries?
Firestore is a NoSQL document database. Each document is a JSON object. Collections group similar documents. Security rules are what separate a professional app from a sieve.
Minimum security rules (but not trivial)
Go to the Firebase Console > Firestore > Rules. Here is a schema we often use for user-based apps:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Only authenticated users can read/write their own data
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Public data (e.g., catalog) readable by all, writable only by admins
match /products/{productId} {
allow read: if true;
allow write: if request.auth != null && request.auth.token.isAdmin == true;
}
}
}You can customize user claims via the Admin SDK (e.g., assign an admin role). We do this often for dashboards and back offices.
Efficient queries: avoid unnecessary reads
Firestore charges per read. Inefficient queries = rising costs. Here is how to write queries that don't drain your wallet:
// Read only recent documents (e.g., orders from the last 30 days)
final now = DateTime.now();
final thirtyDaysAgo = now.subtract(Duration(days: 30));
QuerySnapshot snapshot = await FirebaseFirestore.instance
.collection('orders')
.where('createdAt', isGreaterThan: thirtyDaysAgo)
.orderBy('createdAt', descending: true)
.limit(50)
.get();Note: to use orderBy, you need a composite index. Firestore will prompt you to create one the first time you run the query. Accept and continue.
Sponsored Protocol
Batch writes for multiple operations
If you need to update multiple documents simultaneously (e.g., create an order and decrement stock), use a batch to ensure atomicity and reduce the number of individual writes:
WriteBatch batch = FirebaseFirestore.instance.batch();
batch.set(orderRef, newOrder);
batch.update(productRef, {'stock': FieldValue.increment(-1)});
await batch.commit();How to implement push notifications with Firebase Cloud Messaging?
Firebase Cloud Messaging (FCM) sends notifications to devices. To display them when the app is in the foreground, you need flutter_local_notifications.
Initial FCM setup
In main.dart:
Future<void> setupFirebaseMessaging() async {
FirebaseMessaging messaging = FirebaseMessaging.instance;
// Request permissions (iOS)
NotificationSettings settings = await messaging.requestPermission(
alert: true,
badge: true,
sound: true,
);
// Get device token
String? token = await messaging.getToken();
print('FCM Token: $token');
// Listen for foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
_showLocalNotification(message);
});
// Listen for messages that open the app from background/terminated
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
// Navigate to a specific screen based on notification data
});
// Handle notification that opened the app from a terminated state
RemoteMessage? initialMessage = await messaging.getInitialMessage();
if (initialMessage != null) {
// Navigate
}
}Displaying local notifications when app is in foreground
Configure flutter_local_notifications:
Future<void> initLocalNotifications() async {
const AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
const DarwinInitializationSettings initializationSettingsIOS =
DarwinInitializationSettings();
const InitializationSettings initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsIOS,
);
await FlutterLocalNotificationsPlugin().initialize(initializationSettings);
}
void _showLocalNotification(RemoteMessage message) {
final FlutterLocalNotificationsPlugin plugin = FlutterLocalNotificationsPlugin();
const AndroidNotificationDetails androidDetails =
AndroidNotificationDetails('channel_id', 'channel_name',
importance: Importance.high);
const NotificationDetails details =
NotificationDetails(android: androidDetails);
plugin.show(0, message.notification?.title, message.notification?.body, details);
}Note: for Android channels, you need to create them in AndroidManifest.xml or at runtime. We create them in the native Application class.
Sponsored Protocol
Sending notifications from the backend (or Cloud Functions)
You can send notifications directly from the Firebase Console for testing, but in production use Cloud Functions or a custom backend. Here is a Node.js function that sends a notification to a specific user:
const admin = require('firebase-admin');
admin.initializeApp();
async function sendNotificationToUser(uid, title, body) {
const userDoc = await admin.firestore().collection('users').doc(uid).get();
const token = userDoc.data()?.fcmToken;
if (!token) return;
const message = {
notification: { title, body },
token: token,
};
await admin.messaging().send(message);
}To save the user's FCM token, update the user document after each login:
Future<void> saveFCMToken(String uid) async {
String? token = await FirebaseMessaging.instance.getToken();
if (token != null) {
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'fcmToken': token,
});
}
}What are the best practices for performance and costs?
Firebase is not magic. If used poorly, costs rise and performance drops. Here are the rules we apply:
Sponsored Protocol
- Always use Limit(). Never run a query without a limit. Without it, Firestore loads all matching documents. With 10,000 documents, that's 10,000 reads every time.
- Index only what you need. Every additional index costs in storage and writes. Create only the indexes required for your most frequent queries.
- Don't store files in Firestore. For images, videos, or attachments, use Firebase Storage. Firestore is for structured data, not blobs.
- Monitor costs in the console. Every month, check the "Usage" section of Firebase. If you see unexplained spikes, you likely have an inefficient query or an attack (e.g., someone reading all your documents). Security rules stop this.
What to do next
Here are the concrete steps to turn this guide into a working app:
- Configure your Firebase project: go to console.firebase.google.com, create a project, add Android/iOS apps, and download the configuration files.
- Add dependencies to
pubspec.yamlas shown above and runflutter pub get. - Implement authentication: start with email/password, then add Google Sign-In. Test on emulator and physical device.
- Set security rules: copy the schema above and adapt it to your collections. Do not skip this step.
- Add Firestore: create a
userscollection and save data after registration. Then build a profile screen that reads its own data. - Integrate push notifications: configure FCM + flutter_local_notifications, test by sending from the Console, then create a Cloud Function for automated sends (e.g., order confirmation).
- Monitor and optimize: after launch, keep an eye on the Usage section. If costs increase, review your most frequent queries.
If you want to dive deeper into the full architecture of a Flutter app with a backend, read our Flutter Pillar.
For a practical case of how Apple is pushing on-device AI (which could integrate with smart push notifications), check out Apple PrismML.