Backend as a Service does not remove architecture

Firebase accelerates identity, real-time data, files, and push. The risk is allowing SDK calls to spread through widgets or treating login as authorization. Keep Firebase at the edge:

presentation -> use cases -> repositories -> Firebase adapters

Create separate projects for development, staging, and production. Client configuration identifies the project; administrative credentials and rules still require protection.

Authentication and session

abstract interface class AuthRepository {
  Stream<AppUser?> authStateChanges();
  Future<AppUser> signInWithEmail(String email, String password);
  Future<void> signOut();
}

The implementation translates FirebaseAuthException into application failures. Login messages should not disclose account existence when that creates an enumeration risk.

Authentication establishes identity. Authorization depends on backend rules, claims, and validated data. Hiding a button does not protect a resource.

Firestore modeling

Design collections around actual read patterns. Firestore is not a generic relational database; controlled duplication can be appropriate when consistency ownership is explicit.

final snapshot = await firestore.collection('products').limit(50).get();
final products = snapshot.docs
    .map((doc) => ProductDto.fromJson({'id': doc.id, ...doc.data()}))
    .map((dto) => dto.toEntity())
    .toList(growable: false);

Plan pagination and indexes, and keep listeners active only while needed. Cost is an architectural requirement too.

Security Rules are production code

Deny by default and grant by identity, role, and ownership. Validate writable fields and types. Test rules in the Emulator Suite.

match /products/{productId} {
  allow read: if request.auth != null;
  allow write: if request.auth != null
    && request.auth.token.role == 'admin';
}

Adapt the rule to the real model and never trust a role supplied by the client.

Safe file uploads

Validate size, allowed type, and path. Generate collision-resistant names and keep only the necessary reference in the domain. Expose progress, cancellation, and interrupted-connection recovery.

Cloud Messaging

Ask for notification permission in context. Tokens rotate, so handle update and removal. Push payloads should not expose sensitive information on a locked screen.

Foreground delivery, background handling, and notification taps are different flows. Push-driven navigation must still validate session and destination.

Offline behavior

Firestore’s offline cache improves continuity but does not solve every business conflict. Indicate unconfirmed writes and test reconnection, revoked permission, and rejected synchronization.

Guided practice: integrate Firebase without moving trust to the client

You will continue Sales Management. Start only when the application still runs with the fake adapters from Module 3: this gives you a safe fallback while Firebase is connected at the infrastructure boundary.

Checkpoint 1 — create an isolated development project

Install the CLIs, authenticate, and configure only a development Firebase project:

npm install -g firebase-tools
dart pub global activate flutterfire_cli
firebase login
flutterfire configure
flutter pub add firebase_core firebase_auth cloud_firestore firebase_storage firebase_messaging

Do not reuse a production project. Commit the generated client configuration only after checking the repository policy; never commit service-account files or administrative keys.

Checkpoint 2 — initialize Firebase before dependency composition

In lib/main.dart, initialize the generated options before creating adapters:

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const ProviderScope(child: SalesManagementApp()));
}

If your project kept Provider instead of Riverpod, preserve its root provider tree; Firebase initialization does not require changing the presentation strategy.

Checkpoint 3 — run against local emulators

Initialize and start Authentication, Firestore, and Storage emulators:

firebase init emulators
firebase emulators:start --only auth,firestore,storage

Connect in development only. Android emulators reach the host through 10.0.2.2; desktop and web normally use localhost:

const host = String.fromEnvironment(
  'EMULATOR_HOST',
  defaultValue: 'localhost',
);

await FirebaseAuth.instance.useAuthEmulator(host, 9099);
FirebaseFirestore.instance.useFirestoreEmulator(host, 8080);
await FirebaseStorage.instance.useStorageEmulator(host, 9199);

Run Android with --dart-define=EMULATOR_HOST=10.0.2.2. The checkpoint is complete when the Emulator Suite UI shows the app connection and no production data is touched.

Checkpoint 4 — implement the authentication adapter

Create lib/features/auth/data/firebase_auth_repository.dart and translate SDK errors at the boundary:

final class FirebaseAuthRepository implements AuthRepository {
  FirebaseAuthRepository(this._auth);
  final FirebaseAuth _auth;

  @override
  Stream<AppUser?> authStateChanges() => _auth.authStateChanges().map(
        (user) => user == null ? null : AppUser(id: user.uid),
      );

  @override
  Future<AppUser> signInWithEmail(String email, String password) async {
    try {
      final result = await _auth.signInWithEmailAndPassword(
        email: email.trim(),
        password: password,
      );
      return AppUser(id: result.user!.uid);
    } on FirebaseAuthException catch (error) {
      throw AuthenticationFailure.fromCode(error.code);
    }
  }

  @override
  Future<void> signOut() => _auth.signOut();
}

The UI receives application failures, not Firebase exception types or messages that reveal whether an account exists.

Checkpoint 5 — deny by default and prove authorization

Create firestore.rules:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /products/{productId} {
      allow read: if request.auth != null;
      allow create, update, delete: if request.auth != null
        && request.auth.token.role == 'admin';
    }
  }
}

In the emulator, prove four cases: anonymous read is denied; authenticated read succeeds; a regular user cannot write; an admin can write a valid product. A hidden edit button is useful feedback, but the rule is the security control.

Checkpoint 6 — add files and messaging deliberately

Implement Storage upload behind a repository, exposing progress and cancellation. Register messaging tokens only after consent, handle rotation, and never log the token or place sensitive content in push payloads.

Common failures:

SymptomLikely causeCheck
Android cannot reach an emulatorlocalhost points to the deviceuse 10.0.2.2
permission-denied for an adminrole claim is absent or staleinspect the emulator user and refresh sign-in
app uses production accidentallyemulator connection ran too lateconnect before repository composition
test passes only through the UIrules were not exercised directlyadd allowed and forbidden emulator cases

Completion gate and independent practice

Run flutter analyze, flutter test, and the four authorization scenarios. Restart the app and confirm that auth state is restored and sign-out returns to the login screen.

Then add an orders collection independently. Define ownership rules so a user can read only their orders, write emulator tests for both the owner and a different user, and explain in the README why client-side filtering would not provide the same protection.

Deeper understanding: trust boundary

Firebase reduces infrastructure operated by the team; it does not remove authorization modeling. The client is outside the trust boundary: submitted fields, displayed roles, and paths built in the app can be modified. Security Rules and trusted functions must validate identity, ownership, transition, and shape.

Model rules together with queries. A query that reads an entire collection and filters on the client is poor for security, cost, and performance. Design indexes and document structure around authorized access patterns.

Use Emulator Suite for reproducible, isolated tests. A test should attempt both allowed and forbidden paths because proving only success does not validate authorization. Production data and credentials do not belong in the lab.

Terms to review: authentication, authorization, trust boundary, Security Rules, claim, emulator, document, and index. See the course reference.