Connecting Firebase Auth & Firestore with Flutter: Step-by-Step Guide
Almost every real app needs two things: a way for users to sign in, and a place to store their data. Firebase Authentication and Cloud Firestore give you both w...
Almost every real app needs two things: a way for users to sign in, and a place to store their data. Firebase Authentication and Cloud Firestore give you both without building or hosting a backend, which is why Firebase and Flutter are one of the most popular pairings for MVPs, student projects, and startup apps.
This guide walks you through the complete setup using the official FlutterFire CLI: creating the project, adding Firebase to your Flutter app, building sign-up and sign-in, saving user data in Firestore, reading it in real time, and locking everything down with security rules.
Quick Answer: How Do You Connect Firebase Auth and Firestore with Flutter?
To connect Firebase Auth and Firestore with Flutter: create a Firebase project, enable Email/Password sign-in and Firestore in the console, run flutterfire configure, add firebase_core, firebase_auth and cloud_firestore, call Firebase.initializeApp() in main(), then use FirebaseAuth to sign users in and FirebaseFirestore to store their data under their uid.
What You Will Build
A small app with:
- Email and password sign-up, sign-in, sign-out and password reset
- A user profile document saved in Firestore when a user registers
- Automatic routing between the login screen and home screen
- A real-time notes list (create, read, update, delete) private to each user
- Firestore security rules so users can only access their own data
Prerequisites
| Requirement | Notes |
|---|---|
| Flutter SDK installed | Run flutter doctor and fix any issues first |
| A Flutter project | flutter create firebase_demo works fine |
| Google account | Needed for the Firebase console |
| Node.js | Required to install the Firebase CLI |
| Basic Dart knowledge | Widgets, async/await, and streams |
Firebase Auth vs Firestore: What Does Each Do?
| Service | What it does | Package |
|---|---|---|
| Firebase Authentication | Handles user identity: sign-up, sign-in, sessions, password reset, social login | firebase_auth |
| Cloud Firestore | NoSQL cloud database with real-time updates and offline support | cloud_firestore |
| Firebase Core | Connects your Flutter app to your Firebase project | firebase_core |
Auth answers "who is this user?" and Firestore answers "what data belongs to them?". The link between the two is the user's uid.
Step 1: Set Up the Firebase Console
- Go to the Firebase console and click Create a project.
- Open Build > Authentication, click Get started, and enable the Email/Password provider.
- Open Build > Firestore Database, click Create database, and choose a region close to your users.
- Start in test mode while developing, and replace it with proper rules in Step 11 before you publish.
Warning: Test mode leaves your database open to anyone for a limited time. Never ship an app with test-mode rules.
Step 2: Install the Firebase CLI and FlutterFire CLI
Run these commands in your terminal:
# Install the Firebase CLI
npm install -g firebase-tools
# Log in to your Google account
firebase login
# Install the FlutterFire CLI
dart pub global activate flutterfire_cli
The Firebase CLI talks to your Firebase account. The FlutterFire CLI (flutterfire) generates the configuration your Flutter app needs, so you no longer have to manually download google-services.json or GoogleService-Info.plist.
Step 3: Connect Your Flutter App to Firebase
From the root of your Flutter project, run:
flutterfire configure
Select your Firebase project and the platforms you want (Android, iOS, web). The CLI registers your apps and generates lib/firebase_options.dart, which holds the platform-specific settings.
Step 4: Add the Firebase Packages
flutter pub add firebase_core
flutter pub add firebase_auth
flutter pub add cloud_firestore
Using flutter pub add installs the latest compatible versions. Keep all FlutterFire packages reasonably up to date together to avoid version conflicts.
Platform notes:
| Platform | Things to check |
|---|---|
| Android | Recent Firebase versions require a higher minSdk (often 23). Set it in android/app/build.gradle if the build complains. |
| iOS | Recent Firebase SDKs need a newer minimum iOS deployment target. Check the FlutterFire docs for the version your plugins require, then run pod install if needed. |
| Web | Works out of the box after flutterfire configure. |
Step 5: Initialize Firebase in main.dart
Firebase must be initialized before runApp(), and main() must be async.
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'auth_gate.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Firebase Demo',
theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.blue),
home: const AuthGate(),
);
}
}
Run the app once. If it launches without a [core/no-app] error, Firebase is connected.
Step 6: Recommended Folder Structure
lib/
├── main.dart
├── firebase_options.dart # generated by FlutterFire CLI
├── auth_gate.dart # routes by auth state
├── services/
│ ├── auth_service.dart # Firebase Auth logic
│ └── notes_service.dart # Firestore CRUD logic
└── screens/
├── login_screen.dart
└── home_screen.dart
Keeping Firebase calls inside service classes instead of widgets makes your code easier to test, reuse, and later move to a state management solution such as Riverpod or Bloc.
Step 7: Create the Auth Service (Sign Up, Sign In, Sign Out)
When a user signs up, we create the Auth account and a matching profile document in Firestore, using the uid as the document ID.
// lib/services/auth_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _db = FirebaseFirestore.instance;
Stream<User?> get authStateChanges => _auth.authStateChanges();
User? get currentUser => _auth.currentUser;
Future<UserCredential> signUp({
required String name,
required String email,
required String password,
}) async {
final cred = await _auth.createUserWithEmailAndPassword(
email: email.trim(),
password: password,
);
final user = cred.user!;
await user.updateDisplayName(name);
// Save the profile in Firestore, keyed by uid
await _db.collection('users').doc(user.uid).set({
'uid': user.uid,
'name': name,
'email': email.trim(),
'createdAt': FieldValue.serverTimestamp(),
});
return cred;
}
Future<UserCredential> signIn({
required String email,
required String password,
}) {
return _auth.signInWithEmailAndPassword(
email: email.trim(),
password: password,
);
}
Future<void> sendPasswordReset(String email) {
return _auth.sendPasswordResetEmail(email: email.trim());
}
Future<void> signOut() => _auth.signOut();
}
Step 8: Route Users Automatically with authStateChanges
authStateChanges() is a stream that emits every time a user signs in or out. Wrap it in a StreamBuilder and your app switches screens on its own, and Firebase also restores the session when the app restarts.
// lib/auth_gate.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'screens/home_screen.dart';
import 'screens/login_screen.dart';
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<User?>(
stream: FirebaseAuth.instance.authStateChanges(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasData) return const HomeScreen();
return const LoginScreen();
},
);
}
}
Step 9: Build the Login and Sign-Up Screen
This single screen toggles between sign-in and sign-up and shows friendly error messages.
// lib/screens/login_screen.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _auth = AuthService();
final _name = TextEditingController();
final _email = TextEditingController();
final _password = TextEditingController();
bool _isLogin = true;
bool _loading = false;
String? _error;
@override
void dispose() {
_name.dispose();
_email.dispose();
_password.dispose();
super.dispose();
}
Future<void> _submit() async {
setState(() {
_loading = true;
_error = null;
});
try {
if (_isLogin) {
await _auth.signIn(email: _email.text, password: _password.text);
} else {
await _auth.signUp(
name: _name.text.trim(),
email: _email.text,
password: _password.text,
);
}
// AuthGate handles navigation automatically.
} on FirebaseAuthException catch (e) {
setState(() => _error = _friendlyMessage(e.code));
} catch (_) {
setState(() => _error = 'Something went wrong. Please try again.');
} finally {
if (mounted) setState(() => _loading = false);
}
}
String _friendlyMessage(String code) {
switch (code) {
case 'email-already-in-use':
return 'This email is already registered.';
case 'weak-password':
return 'Password should be at least 6 characters.';
case 'invalid-email':
return 'Please enter a valid email address.';
case 'user-not-found':
case 'wrong-password':
case 'invalid-credential':
return 'Incorrect email or password.';
case 'network-request-failed':
return 'No internet connection.';
default:
return 'Authentication failed ($code).';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_isLogin ? 'Welcome back' : 'Create account',
style: Theme.of(context).textTheme.headlineMedium,
),
const SizedBox(height: 24),
if (!_isLogin) ...[
TextField(
controller: _name,
decoration: const InputDecoration(labelText: 'Name'),
),
const SizedBox(height: 12),
],
TextField(
controller: _email,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(labelText: 'Email'),
),
const SizedBox(height: 12),
TextField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
),
if (_error != null) ...[
const SizedBox(height: 12),
Text(_error!, style: const TextStyle(color: Colors.red)),
],
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _loading ? null : _submit,
child: _loading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(_isLogin ? 'Sign in' : 'Sign up'),
),
),
TextButton(
onPressed: () => setState(() => _isLogin = !_isLogin),
child: Text(_isLogin
? "Don't have an account? Sign up"
: 'Already have an account? Sign in'),
),
if (_isLogin)
TextButton(
onPressed: () async {
if (_email.text.trim().isEmpty) {
setState(() => _error = 'Enter your email first.');
return;
}
await _auth.sendPasswordReset(_email.text);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Password reset email sent.')),
);
},
child: const Text('Forgot password?'),
),
],
),
),
),
),
);
}
}
Note: Firebase projects with email enumeration protection return the generic invalid-credential error for wrong emails and wrong passwords, so handle it as shown above.
Step 10: Read and Write Data with Cloud Firestore
The Data Model
Firestore stores data in collections of documents. We use this structure so each user's data lives under their own uid:
users (collection)
└── {uid} (document) → name, email, createdAt
└── notes (subcollection)
└── {noteId} → text, createdAt
Notes Service (CRUD)
// lib/services/notes_service.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
class NotesService {
final _db = FirebaseFirestore.instance;
CollectionReference<Map<String, dynamic>> get _notes {
final uid = FirebaseAuth.instance.currentUser!.uid;
return _db.collection('users').doc(uid).collection('notes');
}
// CREATE
Future<void> addNote(String text) {
return _notes.add({
'text': text,
'createdAt': FieldValue.serverTimestamp(),
});
}
// READ (real-time)
Stream<QuerySnapshot<Map<String, dynamic>>> watchNotes() {
return _notes.orderBy('createdAt', descending: true).snapshots();
}
// UPDATE
Future<void> updateNote(String id, String text) {
return _notes.doc(id).update({'text': text});
}
// DELETE
Future<void> deleteNote(String id) {
return _notes.doc(id).delete();
}
}
Home Screen: Profile and Live Notes List
// lib/screens/home_screen.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
import '../services/notes_service.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final _notes = NotesService();
final _controller = TextEditingController();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _add() async {
final text = _controller.text.trim();
if (text.isEmpty) return;
await _notes.addNote(text);
_controller.clear();
}
@override
Widget build(BuildContext context) {
final uid = FirebaseAuth.instance.currentUser!.uid;
return Scaffold(
appBar: AppBar(
title: const Text('My Notes'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => AuthService().signOut(),
),
],
),
body: Column(
children: [
// Profile, read in real time from Firestore
StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance
.collection('users')
.doc(uid)
.snapshots(),
builder: (context, snapshot) {
if (snapshot.hasError) return const Text('Could not load profile');
if (!snapshot.hasData) return const LinearProgressIndicator();
final data = snapshot.data!.data();
if (data == null) return const Text('Profile not found');
return ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text(data['name'] ?? ''),
subtitle: Text(data['email'] ?? ''),
);
},
),
const Divider(),
// Add note
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(hintText: 'Write a note'),
onSubmitted: (_) => _add(),
),
),
IconButton(icon: const Icon(Icons.add), onPressed: _add),
],
),
),
// Notes list, real time
Expanded(
child: StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
stream: _notes.watchNotes(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text('Something went wrong'));
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final docs = snapshot.data!.docs;
if (docs.isEmpty) {
return const Center(child: Text('No notes yet'));
}
return ListView.builder(
itemCount: docs.length,
itemBuilder: (context, i) {
final doc = docs[i];
return ListTile(
title: Text(doc['text'] ?? ''),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _notes.deleteNote(doc.id),
),
);
},
);
},
),
),
],
),
);
}
}
Step 11: Lock Down Firestore with Security Rules
Before publishing, replace test-mode rules. Open Firestore Database > Rules in the console and paste:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
match /notes/{noteId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
}
}
}
These rules mean a signed-in user can read and write only their own profile and notes. Everything else is denied by default. Test them with the Rules Playground in the console before you publish.
Step 12: Test the Full Flow
- Run the app and sign up with a new email.
- Open the Firebase console and confirm the user appears under Authentication > Users.
- Check Firestore Database for a new users/{uid} document.
- Add a note and watch it appear instantly. Add one from the console and it shows in the app too.
- Restart the app. You should stay signed in.
- Sign out and confirm you return to the login screen.
Common Errors and How to Fix Them
| Error | Likely cause | Fix |
|---|---|---|
| [core/no-app] No Firebase App '[DEFAULT]' has been created | initializeApp not awaited or not called | Call await Firebase.initializeApp(...) in main() before runApp() |
| firebase_options.dart not found | flutterfire configure was not run | Run it from the project root |
| permission-denied | Security rules block the request | Check that the user is signed in and rules match your data path |
| email-already-in-use | Account already exists | Show a sign-in prompt instead |
| invalid-credential | Wrong email or password | Show a generic "incorrect email or password" message |
| network-request-failed | No internet or emulator has no network | Check connection and emulator settings |
| Android build fails on minSdk | Firebase requires a higher minimum SDK | Raise minSdk in android/app/build.gradle |
| Firestore data not showing | Wrong collection path or missing index | Verify path and follow the index link in the console error |
Best Practices for Production
- Never ship test-mode rules. Write rules from day one.
- Keep Firebase logic in service classes, not inside widgets.
- Use snapshots() for live data and get() for one-time reads to save reads and cost.
- Add pagination (limit() and startAfter()) to large lists.
- Store per-user data under the user's uid so rules stay simple.
- Verify emails with sendEmailVerification() if your app needs real users.
- Enable Firebase App Check to protect your backend from abuse.
- Handle errors and loading states for every stream and future.
- Set budget alerts if you move to a paid plan.
Firebase Pricing: Is It Free?
Firebase has a free Spark plan that is enough for learning, prototypes, and many early MVPs, and a pay-as-you-go Blaze plan that is billed on usage (reads, writes, storage, and bandwidth). Limits and prices change over time, so always check Firebase's official pricing page before budgeting a production app. To estimate the full project budget, see our guide on how much it costs to build a Flutter app.
Next Steps: Add More Firebase Features
Once Auth and Firestore work, these are the common upgrades:
| Feature | Package | Use case |
|---|---|---|
| Push notifications | firebase_messaging | Alerts, reminders, marketing messages |
| Crash reporting | firebase_crashlytics | Find and fix production crashes |
| Analytics | firebase_analytics | Track user behavior |
| Google / Apple sign-in | firebase_auth + provider packages | One-tap login |
| File storage | firebase_storage | Profile photos and uploads |
For Students and Course Leads: Suggested Lesson Plan
| Session | Topic | Outcome |
|---|---|---|
| 1 | Firebase project, FlutterFire CLI, initialization | App connects to Firebase |
| 2 | Email/password auth and error handling | Working sign-up and sign-in |
| 3 | Auth state routing and sessions | Persistent login |
| 4 | Firestore data modeling and CRUD | Live notes list |
| 5 | Security rules and testing | Safe, private data per user |
Is Firebase the Right Backend for Your Flutter App?
Firebase is ideal for MVPs, prototypes, chat and social features, and apps that need real-time updates with a small team. It can be less suitable for apps with complex relational queries or strict data-residency needs, where a SQL backend such as Supabase or PostgreSQL may fit better. If you are still choosing your framework, our comparison of Flutter vs React Native for startup app development explains when each one makes sense.
Frequently Asked Questions
How do I connect Firebase to a Flutter app?
Create a Firebase project, install the Firebase CLI and FlutterFire CLI, run flutterfire configure in your project, add firebase_core, and call Firebase.initializeApp() in main().
How do I connect Firebase Auth and Firestore together in Flutter?
Create the user with createUserWithEmailAndPassword, then save a document in Firestore using the user's uid as the document ID. Use that same uid to read and secure their data later.
What is the FlutterFire CLI?
The FlutterFire CLI is a command-line tool that registers your Flutter apps with Firebase and generates the firebase_options.dart file, so you do not have to configure each platform by hand.
Do I need firebase_core if I use firebase_auth and cloud_firestore?
Yes. firebase_core is required and must be initialized before you use any other Firebase plugin.
How do I keep users signed in after restarting the app?
Firebase Auth persists the session automatically. Listen to authStateChanges() and it will emit the signed-in user on the next launch.
What is the difference between snapshots() and get() in Firestore?
snapshots() returns a stream that updates in real time, while get() fetches the data once. Use snapshots() for live screens and get() for one-time reads.
Is Firebase free for Flutter apps?
Firebase offers a free Spark plan suitable for learning and small apps, and a usage-based Blaze plan for growth. Check the official pricing page for current limits.
Why do I get a permission-denied error in Firestore?
Your security rules are rejecting the request. Confirm the user is signed in and that your rules allow access to the exact path you are reading or writing.
Can I use Firebase with Flutter web and desktop?
Yes. Firebase supports Android, iOS, and web through FlutterFire. Support for desktop platforms varies by plugin, so check each package's documentation.
Is Firebase good for beginners learning Flutter?
Yes. It removes the need to build a server, so beginners can focus on Flutter UI, state, and data flow while still building a real, working app.
Final Takeaway
Connecting Firebase Auth and Firestore with Flutter takes four moves: configure with FlutterFire, initialize in main(), authenticate users with firebase_auth, and store their data in cloud_firestore under their uid. Add proper security rules before launch and you have a secure, scalable foundation for your app.
Need help building a production-ready Flutter app with Firebase? Talk to our Flutter team for a free consultation and a clear, itemized quote.