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:
| Identidade | Ler produto | Criar/alterar | Excluir |
|---|---|---|---|
| anônimo | negar | negar | negar |
| usuário comum | permitir | negar | negar |
| administrador | permitir | permitir payload válido | permitir |
| administrador, preço negativo | — | negar | — |
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
| Sintoma | Causa provável |
|---|---|
| dados aparecem no projeto remoto | conexão aos emuladores ocorreu tarde ou flag está falsa |
Android não alcança localhost | host precisa ser 10.0.2.2 no emulador padrão |
| UI esconde botão, mas escrita funciona | regra de backend está permissiva |
| consulta recebe permission-denied | consulta e regra descrevem conjuntos diferentes |
| testes passam com regra aberta | apenas 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.