DevTools

Cheatsheet Firebase / Firestore

Plataforma NoSQL da Google para apps em tempo real e cloud

Voltar às linguagens
Firebase / Firestore
64 cards encontrados
Categorias:
Versões:

Setup e Configuração


8 cards
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


8 cards
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


8 cards
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


8 cards
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


8 cards
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 = null

signOut() 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


8 cards
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


8 cards
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


8 cards
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 nada

writeBatch() 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.