# What You’ll Build (and Why It Matters)#
This flutter firebase tutorial walks you from a blank Flutter project to a deployed app with Firebase Authentication, Firestore, and Cloud Functions. You’ll end with an app that can sign users in, store per-user data securely, and run server-side logic you can’t trust to the client.
Firebase is popular because it removes months of backend work, but production apps still fail on basics like security rules, indexing, and data modeling. This guide focuses on the practical parts that matter for real deployments, not just “it runs on my phone”.
If you’re evaluating cross-platform options, compare the tradeoffs in Flutter vs React Native (2026). If you need a team to ship faster, Samioda builds production apps with Flutter and automation stacks: mobile & web development.
# Prerequisites (Tools + Accounts)#
| Requirement | Version (recommended) | Notes |
|---|---|---|
| Flutter SDK | Stable channel (latest) | flutter doctor should be green |
| Dart | Bundled with Flutter | No separate install needed |
| Android Studio / Xcode | Latest stable | For emulators/simulators |
| Firebase account | Google account | You’ll create a project + apps |
| Node.js | 18+ LTS | Needed for Firebase CLI + Functions |
| Firebase CLI | Latest | Deploy Functions, configure hosting (optional) |
Install Firebase CLI:
npm i -g firebase-tools
firebase loginCreate and verify your Flutter app:
flutter create flutter_firebase_2026
cd flutter_firebase_2026
flutter runℹ️ Note: This tutorial uses Flutter’s modern Firebase approach via
flutterfireCLI to generate platform configs. That avoids manual config mistakes and keeps secrets out of source control.
# Step 1: Create Firebase Project + Register Apps#
1) Create the Firebase project#
- 1Go to Firebase Console → Add project
- 2Choose a project name (e.g.,
flutter-firebase-2026) - 3Enable Google Analytics only if you need it now (you can add later)
2) Register Android and iOS apps#
In the Firebase project:
- Add an Android app with package name like
com.example.flutter_firebase_2026 - Add an iOS app with bundle ID like
com.example.flutterFirebase2026
For Android, set your applicationId in android/app/build.gradle (if you want a custom ID). For iOS, set your bundle ID in Xcode (Runner target settings) or in your project template.
3) Use FlutterFire CLI to configure#
Install FlutterFire CLI:
dart pub global activate flutterfire_cliRun configuration:
flutterfire configureThis generates lib/firebase_options.dart and adds native config files in the right places.
# Step 2: Add Firebase Dependencies to Flutter#
Add packages:
flutter pub add firebase_core firebase_auth cloud_firestore
flutter pub add flutter_riverpodInitialize Firebase in 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());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(body: Center(child: Text('Firebase ready'))),
);
}
}At this point, run:
flutter runIf you see “Firebase ready”, you’ve completed the baseline setup.
💡 Tip: Add a “smoke test” at this stage (one device + one emulator). It’s cheaper to fix native config issues now than after you add auth, rules, and functions.
# Step 3: Firebase Authentication (Email/Password)#
Authentication is your foundation for access control. Without auth, Firestore rules often become overly permissive “just for testing” and accidentally ship to production.
1) Enable sign-in methods#
Firebase Console → Authentication → Sign-in method:
- Enable Email/Password
2) Implement a minimal auth service#
Create lib/auth/auth_service.dart:
import 'package:firebase_auth/firebase_auth.dart';
class AuthService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Stream<User?> authStateChanges() => _auth.authStateChanges();
Future<UserCredential> signUp(String email, String password) {
return _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
}
Future<UserCredential> signIn(String email, String password) {
return _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
}
Future<void> signOut() => _auth.signOut();
}3) Build simple UI: sign-in / sign-up#
Create lib/auth/auth_screen.dart:
import 'package:flutter/material.dart';
import 'auth_service.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
final _auth = AuthService();
final _email = TextEditingController();
final _password = TextEditingController();
String? _error;
Future<void> _run(Future<void> Function() action) async {
setState(() => _error = null);
try {
await action();
} catch (e) {
setState(() => _error = e.toString());
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sign in')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
TextField(controller: _email, decoration: const InputDecoration(labelText: 'Email')),
TextField(
controller: _password,
decoration: const InputDecoration(labelText: 'Password'),
obscureText: true,
),
const SizedBox(height: 12),
if (_error != null) Text(_error!, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () => _run(() async {
await _auth.signIn(_email.text.trim(), _password.text.trim());
}),
child: const Text('Sign in'),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: () => _run(() async {
await _auth.signUp(_email.text.trim(), _password.text.trim());
}),
child: const Text('Sign up'),
),
),
],
),
],
),
),
);
}
}4) Route users based on auth state#
Replace home: in MyApp with an auth gate.
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'auth/auth_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.data == null) return const AuthScreen();
return const Scaffold(body: Center(child: Text('Logged in')));
},
);
}
}And set:
home: const AuthGate(),⚠️ Warning: Don’t show “logged in” content before
authStateChanges()resolves. Otherwise you’ll get flicker and, worse, briefly render screens that assume a user exists.
# Step 4: Firestore Data Model + Security Rules#
Firestore is flexible, but flexibility creates messy schemas. Start with a clean, minimal model.
Data model: per-user “todos”#
We’ll store todos under a user:
users/{uid}(profile)users/{uid}/todos/{todoId}(items)
This structure makes security rules straightforward: a user can access only their own subtree.
| Collection/Doc | Path | Example fields |
|---|---|---|
| User profile | users/{uid} | email, createdAt |
| Todo item | users/{uid}/todos/{todoId} | title, done, createdAt, updatedAt |
1) Create Firestore database#
Firebase Console → Firestore Database → Create database. Choose a region close to your users (latency matters; even 80–150ms per request adds up).
2) Set Firestore security rules (production baseline)#
Firestore → Rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
match /users/{userId} {
allow read, write: if isSignedIn() && request.auth.uid == userId;
match /todos/{todoId} {
allow read, write: if isSignedIn() && request.auth.uid == userId;
}
}
}
}This locks data to the authenticated user. It’s not “enterprise RBAC”, but it prevents the most common Firebase breach: global read access.
3) Build Firestore CRUD service#
Create lib/data/todo_service.dart:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
class TodoService {
final _db = FirebaseFirestore.instance;
String get _uid => FirebaseAuth.instance.currentUser!.uid;
CollectionReference<Map<String, dynamic>> get _todos =>
_db.collection('users').doc(_uid).collection('todos');
Stream<QuerySnapshot<Map<String, dynamic>>> watchTodos() {
return _todos.orderBy('createdAt', descending: true).snapshots();
}
Future<void> addTodo(String title) {
final now = FieldValue.serverTimestamp();
return _todos.add({
'title': title,
'done': false,
'createdAt': now,
'updatedAt': now,
});
}
Future<void> toggleDone(String id, bool done) {
return _todos.doc(id).update({
'done': done,
'updatedAt': FieldValue.serverTimestamp(),
});
}
Future<void> deleteTodo(String id) => _todos.doc(id).delete();
}🎯 Key Takeaway: Use
serverTimestamp()for audit fields. Client timestamps drift (and can be manipulated), which breaks sorting and can cause confusing UI bugs.
4) Build the todos screen#
Create lib/todos/todos_screen.dart:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import '../data/todo_service.dart';
import 'package:firebase_auth/firebase_auth.dart';
class TodosScreen extends StatefulWidget {
const TodosScreen({super.key});
@override
State<TodosScreen> createState() => _TodosScreenState();
}
class _TodosScreenState extends State<TodosScreen> {
final _service = TodoService();
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Todos'),
actions: [
IconButton(
onPressed: () => FirebaseAuth.instance.signOut(),
icon: const Icon(Icons.logout),
),
],
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: TextField(
controller: _controller,
decoration: const InputDecoration(hintText: 'Add a todo...'),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () async {
final text = _controller.text.trim();
if (text.isEmpty) return;
_controller.clear();
await _service.addTodo(text);
},
child: const Text('Add'),
),
],
),
),
Expanded(
child: StreamBuilder<QuerySnapshot<Map<String, dynamic>>>(
stream: _service.watchTodos(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
final docs = snapshot.data!.docs;
if (docs.isEmpty) return const Center(child: Text('No todos yet.'));
return ListView.builder(
itemCount: docs.length,
itemBuilder: (context, i) {
final d = docs[i];
final data = d.data();
final title = (data['title'] ?? '') as String;
final done = (data['done'] ?? false) as bool;
return Dismissible(
key: ValueKey(d.id),
background: Container(color: Colors.red),
onDismissed: (_) => _service.deleteTodo(d.id),
child: CheckboxListTile(
value: done,
title: Text(title),
onChanged: (v) => _service.toggleDone(d.id, v ?? false),
),
);
},
);
},
),
),
],
),
);
}
}Then update the auth gate to show TodosScreen when logged in.
# Step 5: Firestore Indexes + Performance Basics#
Firestore performance issues usually come from:
- unbounded queries (no limits),
- missing composite indexes,
- chatty UI (too many listeners),
- oversized documents.
Add query limits#
If your list can grow, add .limit(50) and paginate later.
return _todos
.orderBy('createdAt', descending: true)
.limit(50)
.snapshots();Know when you need an index#
If you query with multiple where clauses + orderBy, Firestore will often require a composite index. In production, that becomes a runtime error unless you created the index.
| Query pattern | Usually needs composite index? | Example |
|---|---|---|
orderBy(createdAt) | No | Recent items |
where(done == false) | No | Filter only |
where(done == false).orderBy(createdAt) | Often yes | Filter + sort |
where(status in [...]).orderBy(priority) | Often yes | Complex lists |
💡 Tip: When Firestore throws an index error, it includes a direct console link to create the required index. Click it, create the index, and commit the query shape into your codebase so it doesn’t change unexpectedly.
# Step 6: Cloud Functions (Server-Side Logic You Can Trust)#
Firestore rules control access, but they’re not a full programming environment. Cloud Functions are where you put privileged logic like:
- assigning roles,
- writing audit logs,
- enforcing cross-document constraints,
- calling external APIs.
1) Initialize Functions#
In your project root:
firebase init functionsChoose:
- Language: JavaScript or TypeScript (TypeScript recommended)
- Use ESLint: optional
- Install dependencies: yes
If you’re using TypeScript, you’ll deploy from functions/.
2) Example function: create user profile on signup#
Create an auth trigger that writes users/{uid} when a user registers.
functions/src/index.ts (TypeScript):
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
admin.initializeApp();
export const onUserCreate = functions.auth.user().onCreate(async (user) => {
const uid = user.uid;
const email = user.email ?? null;
await admin.firestore().collection("users").doc(uid).set({
email,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
}, { merge: true });
});Deploy:
firebase deploy --only functions3) Example callable function: set “admin” claim (restricted)#
Don’t let clients assign roles. This function can be used only by existing admins.
export const setAdminClaim = functions.https.onCall(async (data, context) => {
const callerUid = context.auth?.uid;
if (!callerUid) throw new functions.https.HttpsError("unauthenticated", "Sign in required.");
const caller = await admin.auth().getUser(callerUid);
const isAdmin = caller.customClaims?.admin === true;
if (!isAdmin) throw new functions.https.HttpsError("permission-denied", "Admin only.");
const targetUid = data.uid as string;
await admin.auth().setCustomUserClaims(targetUid, { admin: true });
return { ok: true };
});On the Flutter side, you’d call it with cloud_functions package when needed (e.g., internal admin app).
⚠️ Warning: Callable functions are not automatically “secure”. Always validate
context.authand authorize the caller. Many Firebase incidents are caused by functions that assume “the app UI prevents abuse”.
# Step 7: App Hardening (Rules, Validation, Error Handling)#
1) Add basic validation in Firestore rules#
Prevent empty titles and oversized strings (cost and UX).
match /users/{userId}/todos/{todoId} {
allow create: if isSignedIn()
&& request.auth.uid == userId
&& request.resource.data.title is string
&& request.resource.data.title.size() > 0
&& request.resource.data.title.size() <= 140;
allow update, delete, read: if isSignedIn() && request.auth.uid == userId;
}2) Avoid storing secrets in the app#
Anything in the client can be extracted. Use Functions for:
- API keys requiring secrecy,
- billing operations,
- user-to-user permission checks.
3) Centralize Firebase error mapping#
FirebaseAuth errors are not user-friendly. Map them.
String authMessage(Object e) {
final s = e.toString();
if (s.contains('wrong-password')) return 'Wrong password.';
if (s.contains('user-not-found')) return 'No user found for that email.';
if (s.contains('email-already-in-use')) return 'Email already in use.';
return 'Something went wrong. Please try again.';
}# Step 8: Deploy (Functions + Production Checklist)#
Flutter apps ship via App Store / Play Store, but your Firebase backend must also be “deployed” and locked down.
1) Deploy Cloud Functions#
You already used:
firebase deploy --only functions2) Confirm rules are not in “test mode”#
In Firestore rules, avoid:
allow read, write: if true;
3) Verify billing plan requirements#
Some Firebase features (and usage levels) require Blaze plan. Don’t discover this during launch week.
| Feature | Typically requires Blaze? | Why it matters |
|---|---|---|
| Firestore basic | Not always | Free tier exists, but limits apply |
| Cloud Functions (some triggers/egress) | Often | External network calls and scale |
| Authentication | Usually no | But SMS/phone has constraints |
| Scheduled Functions | Often | Common for batch jobs |
4) Add basic monitoring#
- Firebase Console → Functions logs
- Consider exporting logs to BigQuery for larger apps
- Set alerting on error spikes
ℹ️ Note: Google’s DORA research consistently shows that teams with strong observability and fast feedback loops ship more frequently with lower change failure rates. In practice: logs and alerts reduce “silent failures” that drain weeks.
# Common Pitfalls (and How to Avoid Them)#
- 1Leaving Firestore in test mode — Lock rules early and build your UI around permission errors.
- 2Flat collections with userId fields — It’s workable, but easier to get wrong. Prefer
users/{uid}/...for per-user data. - 3Relying on the client for security — Anything the client can do, an attacker can do. Use rules + functions.
- 4No indexes strategy — If you add “filters + sorting” screens, plan and document query shapes so indexes don’t surprise you.
- 5Oversized documents — Keep documents small; store large blobs in Cloud Storage and reference URLs in Firestore.
For teams that want production help (architecture, rules review, CI/CD, cost controls), see Samioda mobile & web development. If you’re still choosing a stack, review Flutter vs React Native (2026).
# Key Takeaways#
- Use FlutterFire CLI to configure Firebase correctly across platforms and avoid fragile manual setup.
- Build auth first, then design Firestore around security rules (e.g.,
users/{uid}/...) so access control stays simple. - Prefer
serverTimestamp()forcreatedAt/updatedAtto prevent sorting bugs and tampering. - Use Cloud Functions for privileged logic (roles, integrations, integrity checks), and always validate
context.auth. - Add limits, indexes, and logging early; most Firebase “scale issues” are query + rules + observability issues.
# Conclusion#
You now have a working Flutter + Firebase app with Authentication, Firestore CRUD, and Cloud Functions—enough to ship an MVP and evolve it safely. The next step is to harden rules, document your query patterns, add monitoring, and plan for pagination and roles as the product grows.
If you want Samioda to accelerate delivery or review your Firebase security and architecture before launch, contact us via mobile & web development.
FAQ
More in Mobile Development
All →How Much Does a Mobile App MVP Cost? Realistic Breakdown (2026)
Mobile app MVP cost explained with real feature-based breakdowns, app-type ranges, and a Flutter vs native comparison to budget your MVP realistically.
How Much Does Flutter App Development Cost in 2026? (Realistic Budgets by App Complexity)
Learn the real flutter app development cost in 2026 with budgets by complexity, a detailed cost table, and a native vs Flutter comparison for iOS and Android.
Flutter State Management in 2026: Riverpod vs Bloc vs Provider
A practical comparison of Riverpod, Bloc, and Provider for flutter state management 2026—performance, DX, testing, architecture, and when to choose each.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
Flutter State Management in 2026: Riverpod vs Bloc vs Provider
A practical comparison of Riverpod, Bloc, and Provider for flutter state management 2026—performance, DX, testing, architecture, and when to choose each.
Flutter App Development Cost in 2026: Complete Pricing Guide
How much does a Flutter app cost in 2026? Complete pricing breakdown by app complexity, features, and development approach. Real cost examples included.
Flutter vs React Native in 2026: Which One Should You Choose?
An in-depth comparison of Flutter and React Native in 2026 — performance, developer experience, ecosystem, and when to use each.