Cheatsheet Firebase / Firestore
Plataforma NoSQL da Google para apps em tempo real e cloud
Firebase / Firestore
Setup e Configuração
Instalar SDK (Web v9+)
npm install firebase
// Importar apenas o necessário (tree-shaking)
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";O SDK modular v9+ permite tree-shaking — só inclui o que usas no bundle. Instala com npm install firebase e importa funções específicas de cada módulo.
Referências (collection e doc)
import { collection, doc } from "firebase/firestore";
// Referência a coleção
const usersRef = collection(db, "users");
// Referência a documento específico
const userRef = doc(db, "users", "user123");
// Referência a subcoleção
const postsRef = collection(db, "users/user123/posts");collection() aponta para uma coleção e doc() para um documento. São apenas referências — nenhuma leitura é feita até chamares getDoc() ou getDocs().
Inicializar app
const firebaseConfig = {
apiKey: "AIza...",
authDomain: "meu-app.firebaseapp.com",
projectId: "meu-app",
storageBucket: "meu-app.appspot.com",
messagingSenderId: "123456",
appId: "1:123:web:abc",
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);O objeto firebaseConfig vem da consola do Firebase. initializeApp() cria a instância e getFirestore() retorna a referência à base de dados.
Múltiplas apps
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
const app1 = initializeApp(config1);
const app2 = initializeApp(config2, "secundaria");
const db1 = getFirestore(app1);
const db2 = getFirestore(app2);Podes ter várias instâncias passando um nome como segundo argumento de initializeApp(). Útil para ligar a projetos Firebase diferentes na mesma app.
Firebase CLI
npm install -g firebase-tools firebase login firebase init // setup do projeto firebase deploy // publicar regras/functions firebase emulators:start // emuladores locais
O firebase-tools é a CLI oficial. firebase init configura o projeto localmente e firebase deploy publica regras, funções e hosting.
Variáveis de ambiente
// .env
VITE_FIREBASE_API_KEY=AIza...
VITE_FIREBASE_PROJECT_ID=meu-app
// firebase.js
const config = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
};Nunca hardcodes credenciais. Usa variáveis de ambiente com prefixo VITE_ (Vite) ou NEXT_PUBLIC_ (Next.js) para expor ao cliente com segurança.
Estrutura de dados
// Firestore: Coleções → Documentos → Campos
users/ // coleção
user123/ // documento (ID: user123)
nome: "Ana" // campo
idade: 30
posts/ // subcoleção
post1/
titulo: "Olá"O Firestore é NoSQL orientado a documentos. Dados organizam-se em coleções que contêm documentos, que podem ter subcoleções aninhadas.
Tipos de dados suportados
await setDoc(doc(db, "tipos", "exemplo"), {
texto: "string",
numero: 42,
float: 3.14,
booleano: true,
nulo: null,
data: new Date(),
geo: new GeoPoint(38.7, -9.1),
lista: [1, 2, 3],
objeto: { aninhado: true },
});O Firestore suporta string, number, boolean, null, Date, GeoPoint, arrays e objetos aninhados. Cada documento tem limite de 1 MB.
CRUD Firestore
Criar com setDoc
import { setDoc, doc } from "firebase/firestore";
await setDoc(doc(db, "users", "u1"), {
nome: "Ana",
email: "ana@mail.com",
idade: 30,
});setDoc() cria ou sobrescreve um documento com o ID especificado. Se o documento já existir, é totalmente substituído (a menos que uses merge: true).
Atualizar (updateDoc)
import { updateDoc, doc, increment } from "firebase/firestore";
await updateDoc(doc(db, "posts", "p1"), {
titulo: "Título editado",
likes: increment(1),
editadoEm: new Date(),
});updateDoc() modifica apenas os campos indicados sem afetar os restantes. increment() soma atomicamente. Lança erro se o documento não existir.
Criar com addDoc (ID automático)
import { addDoc, collection } from "firebase/firestore";
const ref = await addDoc(collection(db, "posts"), {
titulo: "Novo post",
conteudo: "Texto aqui...",
likes: 0,
criado: new Date(),
});
console.log("ID gerado:", ref.id);addDoc() gera automaticamente um ID único. Ideal quando não precisas de controlar o identificador. Retorna a DocumentReference com o ref.id gerado.
Merge (setDoc parcial)
import { setDoc, doc } from "firebase/firestore";
// Só atualiza campos indicados, mantém os outros
await setDoc(
doc(db, "users", "u1"),
{ idade: 31, cidade: "Lisboa" },
{ merge: true }
);Com { merge: true }, o setDoc() faz update parcial em vez de sobrescrever. Cria o documento se não existir. Útil para updates sem conhecer todos os campos.
Ler documento (getDoc)
import { getDoc, doc } from "firebase/firestore";
const snap = await getDoc(doc(db, "users", "u1"));
if (snap.exists()) {
console.log("ID:", snap.id);
console.log("Dados:", snap.data());
} else {
console.log("Documento não existe");
}getDoc() retorna um DocumentSnapshot. Usa snap.exists() para verificar se existe e snap.data() para obter os campos como objeto.
Apagar documento (deleteDoc)
import { deleteDoc, doc } from "firebase/firestore";
await deleteDoc(doc(db, "users", "u1"));
// Apagar campo específico (sem remover doc)
import { updateDoc, deleteField } from "firebase/firestore";
await updateDoc(doc(db, "users", "u2"), {
campoAntigo: deleteField(),
});deleteDoc() remove o documento inteiro. Para apagar apenas um campo, usa deleteField() dentro de updateDoc(). Subcoleções não são apagadas automaticamente.
Ler coleção (getDocs)
import { getDocs, collection } from "firebase/firestore";
const snap = await getDocs(collection(db, "users"));
console.log("Total:", snap.size);
snap.forEach((doc) => {
console.log(doc.id, "=>", doc.data());
});getDocs() retorna um QuerySnapshot com todos os documentos. snap.size dá o total e snap.forEach() itera cada documento.
Campos especiais
import {
serverTimestamp, arrayUnion,
arrayRemove, increment
} from "firebase/firestore";
await updateDoc(ref, {
atualizado: serverTimestamp(),
tags: arrayUnion("javascript"),
removidas: arrayRemove("antigo"),
views: increment(1),
});serverTimestamp() usa a hora do servidor. arrayUnion() adiciona sem duplicar, arrayRemove() remove e increment() soma atomicamente.
Consultas
where (filtros básicos)
import { query, where, getDocs } from "firebase/firestore";
const q = query(
collection(db, "users"),
where("idade", ">=", 18),
where("ativo", "==", true)
);
const snap = await getDocs(q);where() filtra documentos por campo. Podes encadear múltiplos where() — funcionam como AND. O primeiro argumento é o campo, o segundo o operador.
Contar documentos (count)
import { getCountFromServer, collection } from "firebase/firestore";
const snap = await getCountFromServer(collection(db, "users"));
console.log("Total:", snap.data().count);
// Com filtro
const q = query(collection(db, "posts"), where("publico", "==", true));
const countSnap = await getCountFromServer(q);getCountFromServer() conta documentos sem os descarregar — mais barato em leituras. Funciona com filtros. O resultado está em snap.data().count.
Operadores disponíveis
where("campo", "==", valor)
where("campo", "!=", valor)
where("campo", ">", 10)
where("campo", ">=", 10)
where("campo", "<", 100)
where("campo", "<=", 100)
where("campo", "in", [1, 2, 3])
where("campo", "not-in", [4, 5])
where("tags", "array-contains", "js")
where("tags", "array-contains-any", ["js", "ts"])Operadores de comparação e de arrays. array-contains verifica se um array inclui o valor. in aceita até 30 valores. != exclui também documentos sem o campo.
Consultas a subcoleções
import { collectionGroup, query, where } from "firebase/firestore";
// Pesquisar em TODAS as subcoleções "comentarios"
const q = query(
collectionGroup(db, "comentarios"),
where("autor", "==", "ana@mail.com")
);
const snap = await getDocs(q);collectionGroup() pesquisa em todas as subcoleções com o mesmo nome, em qualquer documento. Requer um índice especial ativado na consola.
Ordenação (orderBy)
import { query, orderBy, getDocs } from "firebase/firestore";
const q = query(
collection(db, "posts"),
orderBy("criado", "desc"),
orderBy("titulo", "asc")
);
const snap = await getDocs(q);orderBy() ordena por um campo. O segundo argumento é "asc" (padrão) ou "desc". Podes encadear para ordenação secundária.
Limitações de consultas
// ❌ Não podes combinar != com orderBy noutro campo // ❌ Máx. 1 filtro de desigualdade por query // ❌ in / array-contains-any: máx. 30 valores // ❌ orderBy + where em campos diferentes → índice composto // ✅ Solução: criar índice composto na consola // ou dividir em duas consultas no cliente
O Firestore tem limitações: só um operador de desigualdade por consulta, e orderBy em campo diferente do where exige índice composto configurado na consola.
Paginação (limit e startAfter)
import { query, orderBy, limit, startAfter } from "firebase/firestore";
// Primeira página
const q1 = query(collection(db, "posts"), orderBy("criado"), limit(10));
const snap1 = await getDocs(q1);
const ultimo = snap1.docs[snap1.docs.length - 1];
// Página seguinte
const q2 = query(collection(db, "posts"), orderBy("criado"), startAfter(ultimo), limit(10));limit() restringe o número de resultados. startAfter() recebe o último documento da página anterior como cursor para paginação eficiente.
startAt e endAt (intervalos)
import { query, orderBy, startAt, endAt } from "firebase/firestore";
// Documentos entre A e M
const q = query(
collection(db, "users"),
orderBy("nome"),
startAt("A"),
endAt("M\uf8ff")
);
const snap = await getDocs(q);startAt() e endAt() definem limites inclusivos. startAfter() e endBefore() são exclusivos. O caractere \uf8ff garante inclusão total do prefixo.
Tempo Real
onSnapshot (documento)
import { onSnapshot, doc } from "firebase/firestore";
const unsub = onSnapshot(doc(db, "users", "u1"), (snap) => {
if (snap.exists()) {
console.log("Dados atuais:", snap.data());
}
});
// Parar de ouvir
unsub();onSnapshot() cria um listener em tempo real. Executa imediatamente com os dados atuais e depois a cada alteração. Retorna uma função unsub() para cancelar.
Metadata (origem dos dados)
onSnapshot(ref, (snap) => {
const fonte = snap.metadata.fromCache ? "cache local" : "servidor";
const pendente = snap.metadata.hasPendingWrites;
console.log(`Fonte: ${fonte} | Escrita pendente: ${pendente}`);
});snap.metadata.fromCache indica se os dados vieram do cache offline. hasPendingWrites mostra se há escritas locais ainda não sincronizadas com o servidor.
onSnapshot (coleção)
const unsub = onSnapshot(collection(db, "mensagens"), (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === "added") {
console.log("Nova:", change.doc.data());
}
if (change.type === "modified") {
console.log("Editada:", change.doc.data());
}
if (change.type === "removed") {
console.log("Removida:", change.doc.id);
}
});
});Em coleções, snapshot.docChanges() mostra exatamente o que mudou: "added", "modified" ou "removed". Mais eficiente que re-renderizar tudo.
Include metadata changes
// Por padrão, não dispara quando só metadata muda
// Para receber também mudanças de metadata:
const unsub = onSnapshot(ref, {
includeMetadataChanges: true,
}, (snap) => {
// Dispara quando dados OU metadata mudam
console.log("fromCache:", snap.metadata.fromCache);
});Com includeMetadataChanges: true, o listener dispara também quando o estado de sincronização muda (ex.: de cache para servidor), não apenas quando os dados mudam.
Listener com query
import { query, where, orderBy, onSnapshot } from "firebase/firestore";
const q = query(
collection(db, "mensagens"),
where("sala", "==", "geral"),
orderBy("criado", "desc")
);
const unsub = onSnapshot(q, (snap) => {
const msgs = snap.docs.map(d => ({ id: d.id, ...d.data() }));
renderizar(msgs);
});Podes passar uma query ao onSnapshot() em vez de uma coleção. O listener só recebe documentos que correspondem ao filtro.
Desligar listeners
// Guardar referência para limpar depois
const listeners = [];
listeners.push(onSnapshot(ref1, cb1));
listeners.push(onSnapshot(ref2, cb2));
// Limpar todos (ex.: onUnmount no React)
function limpar() {
listeners.forEach((unsub) => unsub());
listeners.length = 0;
}
// React: useEffect(() => { return unsub; }, []);Cada onSnapshot() conta como uma leitura ativa. Sempre que o componente é destruído, chama unsub() para evitar memory leaks e leituras desnecessárias.
Tratamento de erros
const unsub = onSnapshot(ref, {
next: (snap) => {
console.log("Dados:", snap.data());
},
error: (err) => {
console.error("Erro no listener:", err.code, err.message);
// permission-denied, unavailable, etc.
},
});Passa um objeto com next e error em vez de callback simples. Erros comuns: permission-denied (regras) e unavailable (rede).
Offline e persistência
import { initializeFirestore, persistentLocalCache } from "firebase/firestore";
// Ativar cache persistente (IndexedDB)
const db = initializeFirestore(app, {
localCache: persistentLocalCache({ tabManager: persistentMultipleTabManager() }),
});
// Escrita offline → sincroniza quando voltar rede
await setDoc(doc(db, "posts", "p1"), { titulo: "Offline" });Com persistentLocalCache(), os dados sobrevivem a reloads. Escritas offline são guardadas localmente e sincronizadas automaticamente quando a conexão volta.
Autenticação
Registar (email/senha)
import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";
const auth = getAuth();
const cred = await createUserWithEmailAndPassword(auth, email, senha);
console.log("UID:", cred.user.uid);createUserWithEmailAndPassword() cria a conta e faz login automático. Retorna UserCredential com cred.user.uid. A senha deve ter mínimo 6 caracteres.
Login social (Google)
import { GoogleAuthProvider, signInWithPopup } from "firebase/auth";
const provider = new GoogleAuthProvider();
provider.addScope("profile");
provider.addScope("email");
const cred = await signInWithPopup(auth, provider);
console.log("Nome:", cred.user.displayName);
console.log("Foto:", cred.user.photoURL);GoogleAuthProvider + signInWithPopup() abre popup de login Google. addScope() pede permissões extra. Também disponível: signInWithRedirect() para mobile.
Login (email/senha)
import { signInWithEmailAndPassword } from "firebase/auth";
try {
const cred = await signInWithEmailAndPassword(auth, email, senha);
console.log("Bem-vindo:", cred.user.email);
} catch (err) {
// auth/invalid-credential (Firebase v10+)
console.error("Erro:", err.code);
}signInWithEmailAndPassword() autentica o utilizador. Erros comuns: auth/invalid-credential, auth/user-not-found, auth/wrong-password.
Reset de password
import { sendPasswordResetEmail } from "firebase/auth";
await sendPasswordResetEmail(auth, "user@mail.com");
// Email de reset enviado
// Após login, alterar password:
import { updatePassword } from "firebase/auth";
await updatePassword(auth.currentUser, "novaSenha123");sendPasswordResetEmail() envia email de recuperação. updatePassword() altera a senha do utilizador logado — requer sessão recente (senão pede re-auth).
Estado de autenticação
import { onAuthStateChanged } from "firebase/auth";
const unsub = onAuthStateChanged(auth, (user) => {
if (user) {
console.log("Logado:", user.uid, user.email);
console.log("Token:", user.accessToken);
} else {
console.log("Sessão terminada");
}
});onAuthStateChanged() é o listener principal de auth. Dispara no load da página e a cada login/logout. Essencial para proteger rotas e mostrar UI condicional.
Dados do perfil
import { updateProfile } from "firebase/auth";
await updateProfile(auth.currentUser, {
displayName: "Ana Silva",
photoURL: "https://exemplo.com/foto.jpg",
});
// Recarregar dados
await auth.currentUser.reload();
console.log(auth.currentUser.displayName);updateProfile() altera displayName e photoURL. Para dados customizados (idade, bio), guarda no Firestore num documento com o mesmo uid.
Logout
import { signOut } from "firebase/auth";
await signOut(auth);
// onAuthStateChanged dispara com user = nullsignOut() termina a sessão. O listener onAuthStateChanged() é notificado automaticamente com user = null. Limpa o estado da app nesse callback.
Tokens e verificação
// Obter token ID (JWT) para enviar a APIs
const token = await auth.currentUser.getIdToken(true);
// No servidor (Node.js Admin SDK):
const admin = require("firebase-admin");
const decoded = await admin.auth().verifyIdToken(token);
console.log("UID verificado:", decoded.uid);getIdToken() retorna um JWT para autenticar pedidos a APIs. No backend, admin.auth().verifyIdToken() valida o token e extrai o uid.
Storage
Upload de ficheiro
import { getStorage, ref, uploadBytes } from "firebase/storage";
const storage = getStorage();
const fileRef = ref(storage, "fotos/imagem.jpg");
const resultado = await uploadBytes(fileRef, ficheiro);
console.log("Path:", resultado.ref.fullPath);uploadBytes() envia o ficheiro de uma vez. O primeiro argumento é a ref() com o caminho no bucket, o segundo é o File ou Blob.
Listar ficheiros
import { listAll, ref } from "firebase/storage";
const lista = await listAll(ref(storage, "fotos/"));
lista.items.forEach((item) => {
console.log("Ficheiro:", item.fullPath);
});
lista.prefixes.forEach((pasta) => {
console.log("Subpasta:", pasta.fullPath);
});listAll() retorna items (ficheiros) e prefixes (subpastas). Para muitas pastas, usa list() com paginação via pageToken.
Obter URL de download
import { getDownloadURL, ref } from "firebase/storage";
const url = await getDownloadURL(ref(storage, "fotos/imagem.jpg"));
// Usar em HTML
imgElement.src = url;
// Ou guardar no Firestore
await updateDoc(docRef, { fotoURL: url });getDownloadURL() retorna um URL temporário com token de acesso. Guarda-o no Firestore para referência futura. O URL muda se o ficheiro for substituído.
Apagar ficheiro
import { deleteObject, ref } from "firebase/storage";
await deleteObject(ref(storage, "fotos/imagem.jpg"));
// Apagar múltiplos
const paths = ["fotos/a.jpg", "fotos/b.jpg"];
await Promise.all(
paths.map((p) => deleteObject(ref(storage, p)))
);deleteObject() remove um ficheiro do bucket. Lança erro object-not-found se não existir. Usa Promise.all() para apagar vários em paralelo.
Upload com progresso
import { uploadBytesResumable } from "firebase/storage";
const task = uploadBytesResumable(fileRef, ficheiro);
task.on("state_changed", {
next: (snap) => {
const pct = (snap.bytesTransferred / snap.totalBytes) * 100;
console.log(`${pct.toFixed(0)}%`);
},
error: (err) => console.error(err),
complete: () => console.log("Upload completo!"),
});uploadBytesResumable() permite monitorizar progresso e pausar/retomar. state_changed dispara a cada chunk enviado. Ideal para ficheiros grandes.
Metadata do ficheiro
import { getMetadata, updateMetadata } from "firebase/storage";
const meta = await getMetadata(fileRef);
console.log(meta.contentType, meta.size, meta.timeCreated);
// Atualizar metadata
await updateMetadata(fileRef, {
contentType: "image/webp",
customMetadata: { autor: "ana" },
});getMetadata() retorna contentType, size, timeCreated. updateMetadata() permite alterar tipo MIME e adicionar customMetadata.
Pausar e retomar upload
const task = uploadBytesResumable(fileRef, ficheiro); // Pausar task.pause(); // Retomar task.resume(); // Cancelar task.cancel();
O objeto UploadTask tem métodos pause(), resume() e cancel(). Útil para conexões lentas ou quando o utilizador navega para outra página.
Upload de string/base64
import { uploadString } from "firebase/storage";
// Data URL (base64)
await uploadString(fileRef, "data:image/png;base64,iVBOR...", "data_url");
// Raw string
await uploadString(ref(storage, "notas.txt"), "Conteúdo aqui", "raw", {
contentType: "text/plain",
});uploadString() envia strings diretamente. Formatos: "raw", "base64", "base64url" e "data_url". Útil para imagens geradas por canvas.
Regras de Segurança
Estrutura básica
rules_version = "2";
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read: if true;
allow write: if request.auth.uid == userId;
}
}
}As regras usam match para definir caminhos e allow para permissões. request.auth.uid é o utilizador autenticado. Sem regra = acesso negado.
Regras em subcoleções
match /posts/{postId} {
allow read: if true;
match /comentarios/{comentId} {
allow read: if true;
allow create: if request.auth != null
&& request.resource.data.postId == postId;
}
}Subcoleções herdam o contexto do match pai. A variável {postId} fica acessível nas regras da subcoleção para validações cruzadas.
Operações granulares
match /posts/{postId} {
allow get: if true; // ler 1 doc
allow list: if true; // listar coleção
allow create: if request.auth != null;
allow update: if request.auth.uid == resource.data.autorId;
allow delete: if false; // ninguém apaga
}read = get + list. write = create + update + delete. Usar operações granulares dá controlo mais fino sobre o acesso.
Funções personalizadas
function isAdmin() {
return request.auth != null
&& request.auth.token.admin == true;
}
function donoDoDoc(userId) {
return request.auth.uid == userId;
}
match /users/{userId} {
allow read: if isAdmin() || donoDoDoc(userId);
allow write: if donoDoDoc(userId);
}function cria helpers reutilizáveis. Reduz repetição e torna as regras mais legíveis. Podem receber parâmetros e aceder a request e resource.
request vs resource
allow update: if // Dados que o cliente quer gravar request.resource.data.titulo is string && request.resource.data.titulo.size() > 0 // Dados atuais no servidor && resource.data.autorId == request.auth.uid;
request.resource.data são os dados novos (a gravar). resource.data são os dados atuais no servidor. Compara ambos para validar alterações.
get() para verificar outros docs
match /posts/{postId} {
allow create: if
request.auth != null
&& get(/databases/$(database)/documents/users/$(request.auth.uid))
.data.plano == "premium";
}get() lê outro documento durante a avaliação da regra. Útil para verificar perfis, subscrições ou permissões guardadas noutro local. Tem custo de leitura.
Validar tipos de dados
allow create: if
request.resource.data.nome is string
&& request.resource.data.idade is int
&& request.resource.data.idade >= 0
&& request.resource.data.email is string
&& request.resource.data.email.matches(".*@.*\\..*")
&& request.resource.data.keys().hasAll(["nome", "email"]);Valida tipos com is string, is int, is bool. matches() aplica regex. keys().hasAll() garante campos obrigatórios.
Regras de Storage
service firebase.storage {
match /b/{bucket}/o {
match /fotos/{userId}/{fileName} {
allow read: if true;
allow write: if request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024
&& request.resource.contentType.matches("image/.*");
}
}
}Regras de Storage usam request.resource.size (bytes) e request.resource.contentType. Limita tamanho e tipo de ficheiro por segurança.
Avançado
Batch writes
import { writeBatch, doc } from "firebase/firestore";
const batch = writeBatch(db);
batch.set(doc(db, "users", "u1"), { nome: "Ana" });
batch.update(doc(db, "users", "u2"), { ativo: false });
batch.delete(doc(db, "temp", "x"));
await batch.commit(); // tudo ou nadawriteBatch() agrupa até 500 operações (set, update, delete) numa única escrita atómica. Ou todas succeedem ou nenhuma é aplicada. Não permite leituras.
Índices compostos
// firestore.indexes.json
{
"indexes": [{
"collectionGroup": "posts",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "autor", "order": "ASCENDING" },
{ "fieldPath": "criado", "order": "DESCENDING" }
]
}]
}Consultas com where + orderBy em campos diferentes exigem índices compostos. Define em firestore.indexes.json e publica com firebase deploy.
Transações (runTransaction)
import { runTransaction, doc } from "firebase/firestore";
await runTransaction(db, async (tx) => {
const snap = await tx.get(doc(db, "contas", "c1"));
if (!snap.exists()) throw "Conta não existe";
const novoSaldo = snap.data().saldo - 50;
if (novoSaldo < 0) throw "Saldo insuficiente";
tx.update(doc(db, "contas", "c1"), { saldo: novoSaldo });
});runTransaction() permite leitura + escrita atómica. Se os dados mudarem durante a execução, o Firestore repete automaticamente (até 5 tentativas).
Emuladores locais
// firebase.json
{
"emulators": {
"firestore": { "port": 8080 },
"auth": { "port": 9099 },
"storage": { "port": 9199 },
"ui": { "enabled": true, "port": 4000 }
}
}
// Ligar SDK ao emulador
import { connectFirestoreEmulator } from "firebase/firestore";
connectFirestoreEmulator(db, "localhost", 8080);Os emuladores simulam Firestore, Auth e Storage localmente sem custos. connectFirestoreEmulator() redireciona o SDK. UI de debug em localhost:4000.
Cloud Functions (triggers)
// functions/index.js
const { onDocumentCreated } = require("firebase-functions/v2/firestore");
exports.onNovoUser = onDocumentCreated("users/{userId}", (event) => {
const dados = event.data.data();
console.log("Novo user:", dados.email);
// Enviar email de boas-vindas, criar doc de perfil, etc.
});Cloud Functions executa código no servidor em resposta a eventos. Triggers: onDocumentCreated, onDocumentUpdated, onDocumentDeleted, onDocumentWritten.
Segurança (boas práticas)
// ❌ NUNCA em produção: allow read, write: if true; // ✅ Sempre autenticar: allow read: if request.auth != null; // ✅ Validar dados de entrada: allow create: if request.resource.data.keys().hasAll(["nome"]); // ✅ Limitar por utilizador: allow write: if request.auth.uid == userId;
Nunca uses allow read, write: if true em produção. Autentica sempre com request.auth, valida dados de entrada e limita acesso por uid.
Admin SDK (servidor)
const admin = require("firebase-admin");
admin.initializeApp();
const db = admin.firestore();
// Criar
await db.collection("users").doc("u1").set({ nome: "Ana" });
// Query
const snap = await db.collection("users").where("ativo", "==", true).get();
// Apagar coleção inteira
const docs = await db.collection("temp").listDocuments();
await Promise.all(docs.map(d => d.delete()));O Admin SDK corre no servidor sem regras de segurança. Usa admin.firestore() em vez de getFirestore(). Ideal para tarefas administrativas e migrações.
Limites e quotas
// Limites principais do Firestore: // - Documento: máx. 1 MB // - Profundidade subcoleções: 100 níveis // - Batch: máx. 500 operações // - Campos por documento: sem limite prático // - Escrita: ~10k writes/seg por BD // - Leitura grátis: 50k/dia (Spark plan)
Conhece os limites: documentos até 1 MB, batch até 500 ops, subcoleções até 100 níveis. O plano gratuito (Spark) dá 50k leituras e 20k escritas por dia.