Backend as a Service não elimina arquitetura

Firebase acelera autenticação, dados em tempo real, arquivos e push. O risco é espalhar SDKs por widgets e confundir login com autorização. Trate Firebase como detalhe externo:

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

Crie projetos separados para desenvolvimento, homologação e produção. Arquivos de configuração identificam o projeto, mas regras e credenciais administrativas continuam exigindo proteção.

Authentication e sessão

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

A implementação converte FirebaseAuthException em falhas do aplicativo. Mensagens de login não devem revelar se um e-mail está cadastrado quando isso criar risco de enumeração.

Autenticação prova identidade. Autorização depende de regras, claims e dados validados no backend. Esconder um botão não protege um recurso.

Firestore e modelagem

Modele coleções a partir das leituras necessárias. Firestore não é um banco relacional genérico; duplicação controlada pode ser adequada, desde que haja responsabilidade pela consistência.

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);

Use paginação, índices planejados e listeners apenas enquanto a tela precisa deles. Custo também é requisito arquitetural.

Security Rules são código de produção

Comece negando e permita por identidade, papel e propriedade. Valide campos alteráveis e tipos. Teste regras no Emulator Suite; uma regra que “parece certa” no console não é evidência suficiente.

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

Adapte a regra ao seu modelo real e evite confiar em valores de papel enviados pelo cliente.

Storage e upload seguro

Valide tamanho, tipo permitido e caminho. Gere nomes que não colidam e salve somente a referência necessária no domínio. Ofereça progresso, cancelamento e tratamento de conexão interrompida.

Cloud Messaging

Permissão de notificação deve ser solicitada no contexto certo. Tokens mudam; registre rotação e remoção. O payload de push não deve conter informação sensível exibida na tela bloqueada.

Trate foreground, background e toque na notificação como fluxos distintos. Navegação disparada por push precisa validar sessão e destino.

Offline e sincronização

Cache offline do Firestore melhora continuidade, mas não resolve todo conflito de negócio. Explique quando o dado ainda não foi confirmado pelo servidor e teste reconexão, permissões revogadas e gravações rejeitadas.

Prática guiada: integre Firebase sem usar produção como laboratório

Este módulo exige Firebase CLI, FlutterFire CLI, Node.js e JDK para os emuladores. Use um projeto Firebase exclusivo de desenvolvimento. Não selecione produção durante flutterfire configure.

Checkpoint 1 — instale ferramentas e registre a configuração

firebase --version
firebase login
dart pub global activate flutterfire_cli
flutter pub add firebase_core firebase_auth cloud_firestore firebase_storage
flutterfire configure --project=SEU_PROJETO_DEV

O comando gera lib/firebase_options.dart e registra apps para as plataformas escolhidas. Esses identificadores não são segredo, mas contas de serviço, chaves administrativas e tokens de usuário são.

Inicialize em main.dart antes de runApp:

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

Use o root compatível com a escolha do Módulo 5 (ProviderScope apenas se Riverpod ficou no projeto).

Checkpoint 2 — inicialize o Emulator Suite

Na raiz:

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

Escolha Auth, Firestore e Storage. O painel costuma estar em localhost:4000. Conecte o app antes da primeira operação:

Future<void> connectFirebaseEmulators() async {
  const useEmulators = bool.fromEnvironment('USE_FIREBASE_EMULATORS');
  if (!useEmulators) return;

  const host = String.fromEnvironment(
    'FIREBASE_EMULATOR_HOST',
    defaultValue: 'localhost',
  );
  await FirebaseAuth.instance.useAuthEmulator(host, 9099);
  FirebaseFirestore.instance.useFirestoreEmulator(host, 8080);
  await FirebaseStorage.instance.useStorageEmulator(host, 9199);
}

No Android Emulator, o host da máquina normalmente é 10.0.2.2, não localhost. Execute:

flutter run \
  --dart-define=USE_FIREBASE_EMULATORS=true \
  --dart-define=FIREBASE_EMULATOR_HOST=10.0.2.2

Checkpoint 2: uma conta criada pelo app aparece no painel local, não no console do projeto remoto.

Checkpoint 3 — implemente autenticação atrás do contrato

Mantenha AuthRepository no domínio e crie FirebaseAuthRepository em data:

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

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

  @override
  Future<AppUser> signInWithEmail(String email, String password) async {
    try {
      final credential = await _auth.signInWithEmailAndPassword(
        email: email.trim(),
        password: password,
      );
      final user = credential.user;
      if (user == null) throw const AuthFailure('Sessão não foi criada');
      return AppUser(id: user.uid, email: user.email);
    } on FirebaseAuthException catch (error) {
      throw AuthFailure.fromCode(error.code);
    }
  }

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

Não mostre “e-mail não existe” e “senha incorreta” como mensagens diferentes em contexto público se isso permitir enumeração de contas.

Checkpoint 4 — modele consulta e regras juntas

Use documentos em products/{productId} e leia somente campos previstos pelo DTO. Comece firestore.rules negando por padrão:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }

    match /products/{productId} {
      allow read: if request.auth != null;
      allow create, update: if request.auth != null
        && request.auth.token.role == 'admin'
        && request.resource.data.keys().hasOnly(['name', 'price', 'updatedAt'])
        && request.resource.data.name is string
        && request.resource.data.price is number
        && request.resource.data.price >= 0;
      allow delete: if request.auth != null
        && request.auth.token.role == 'admin';
    }
  }
}

Regras não filtram resultados depois da leitura: a consulta precisa ser compatível com o conjunto que o usuário pode acessar. Use claims apenas emitidas por ambiente confiável; nunca aceite role enviado livremente pelo cliente.

Checkpoint 5 — prove permissão permitida e negada

Inicialize testes de rules e execute-os com:

firebase emulators:exec --only firestore "npm test"

Sua matriz mínima:

IdentidadeLer produtoCriar/alterarExcluir
anônimonegarnegarnegar
usuário comumpermitirnegarnegar
administradorpermitirpermitir payload válidopermitir
administrador, preço negativonegar

Testar apenas sucesso não valida autorização. O critério é o backend negar mesmo quando o cliente tenta a operação diretamente.

Checkpoint 6 — Storage e Messaging são fluxos próprios

Para upload, valide tamanho e tipo antes de iniciar, use caminho derivado de identidade/ID controlado, acompanhe snapshotEvents, permita cancelar e grave apenas a referência necessária no documento.

Ao adicionar Messaging, solicite permissão no contexto, trate rotação de token e nunca registre o token completo. Foreground, background e toque na notificação são três entradas; todas precisam validar sessão e destino antes de navegar.

Checkpoint 7 — confirme separação de ambientes

Crie um checklist por dev, staging e production: project ID, apps registrados, rules, índices, buckets, funções e analytics. Execute novamente flutterfire configure quando adicionar plataforma ou produto que exija atualização.

Gates locais:

dart format lib test
flutter analyze
flutter test
firebase emulators:exec --only auth,firestore,storage "npm test"

O último comando depende da suíte de testes de rules. Nenhum gate local autoriza deploy em produção.

Erros comuns

SintomaCausa provável
dados aparecem no projeto remotoconexão aos emuladores ocorreu tarde ou flag está falsa
Android não alcança localhosthost precisa ser 10.0.2.2 no emulador padrão
UI esconde botão, mas escrita funcionaregra de backend está permissiva
consulta recebe permission-deniedconsulta e regra descrevem conjuntos diferentes
testes passam com regra abertaapenas caminhos permitidos foram testados

Prática autônoma: implemente upload de imagem no Storage Emulator com progresso e cancelamento, e uma rule que restringe caminho, proprietário, content type e tamanho.

Aprofundamento: fronteira de confiança

Firebase reduz infraestrutura operada pelo time, não a necessidade de modelar autorização. O cliente está fora da fronteira de confiança: campos enviados, papel exibido e caminhos construídos no aplicativo podem ser modificados. Security Rules e funções confiáveis precisam validar identidade, propriedade, transição e formato.

Modele regras junto com consultas. Uma consulta que exige ler toda a coleção para depois filtrar no cliente é ruim para segurança, custo e desempenho. Crie índices e estrutura de documentos pensando nos acessos autorizados.

Use Emulator Suite para testes reproduzíveis e isolados. Um teste deve tentar o caminho permitido e o proibido, porque provar somente sucesso não valida autorização. Dados e credenciais de produção não pertencem ao laboratório.

Termos para revisar: authentication, authorization, trust boundary, Security Rules, claim, emulator, document e index. Consulte a referência do curso.