Back to Blog
Mobile Apps

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...

EAGLE Solutions
EAGLE SolutionsEditorial Team
21 September 2026 16 min read
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 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

  1. Go to the Firebase console and click Create a project.
  2. Open Build > Authentication, click Get started, and enable the Email/Password provider.
  3. Open Build > Firestore Database, click Create database, and choose a region close to your users.
  4. 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

  1. Run the app and sign up with a new email.
  2. Open the Firebase console and confirm the user appears under Authentication > Users.
  3. Check Firestore Database for a new users/{uid} document.
  4. Add a note and watch it appear instantly. Add one from the console and it shows in the app too.
  5. Restart the app. You should stay signed in.
  6. 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.

Ready to start building?

Join our practical tech courses and turn knowledge into real-world skills.

Explore All Courses