DevTools

Cheatsheet Firebase / Firestore

Plataforma NoSQL da Google para apps em tempo real e cloud

Volver a los lenguajes
Firebase / Firestore
64 tarjetas encontradas
Categorías:
Versiones:

Setup e Configuração


8 cards
Instalar SDK (Web v9+)
npm install firebase

// Importar solo lo necesario (tree-shaking)
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";

El SDK modular v9+ permite tree-shaking — solo incluye lo que usas en el bundle. Instala con npm install firebase e importa funciones específicas de cada módulo.

Referencias (collection y doc)
import { collection, doc } from "firebase/firestore";

// Referencia a colección
const usersRef = collection(db, "users");

// Referencia a documento específico
const userRef = doc(db, "users", "user123");

// Referencia a subcolección
const postsRef = collection(db, "users/user123/posts");

collection() apunta a una colección y doc() a un documento. Son solo referencias — no se hace ninguna lectura hasta llamar a getDoc() o getDocs().

Inicializar App
const firebaseConfig = {
  apiKey: "AIza...",
  authDomain: "my-app.firebaseapp.com",
  projectId: "my-app",
  storageBucket: "my-app.appspot.com",
  messagingSenderId: "123456",
  appId: "1:123:web:abc",
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

El objeto firebaseConfig viene de la consola de Firebase. initializeApp() crea la instancia y getFirestore() retorna la referencia a la base de datos.

Múltiples Apps
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const app1 = initializeApp(config1);
const app2 = initializeApp(config2, "secondary");

const db1 = getFirestore(app1);
const db2 = getFirestore(app2);

Puedes tener varias instancias pasando un nombre como segundo argumento de initializeApp(). Útil para conectar a proyectos Firebase diferentes en la misma app.

Firebase CLI
npm install -g firebase-tools
firebase login
firebase init          // setup del proyecto
firebase deploy        // publicar reglas/functions
firebase emulators:start  // emuladores locales

firebase-tools es la CLI oficial. firebase init configura el proyecto localmente y firebase deploy publica reglas, funciones y hosting.

Variables de Entorno
// .env
VITE_FIREBASE_API_KEY=AIza...
VITE_FIREBASE_PROJECT_ID=my-app

// firebase.js
const config = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
};

Nunca hardcodees credenciales. Usa variables de entorno con prefijo VITE_ (Vite) o NEXT_PUBLIC_ (Next.js) para exponerlas al cliente con seguridad.

Estructura de Datos
// Firestore: Colecciones → Documentos → Campos
users/              // colección
  user123/          // documento (ID: user123)
    name: "Ana"     // campo
    age: 30
    posts/          // subcolección
      post1/
        title: "Hola"

Firestore es NoSQL orientado a documentos. Los datos se organizan en colecciones que contienen documentos, que pueden tener subcolecciones anidadas.

Tipos de Datos Soportados
await setDoc(doc(db, "types", "example"), {
  text: "string",
  number: 42,
  float: 3.14,
  boolean: true,
  nothing: null,
  date: new Date(),
  geo: new GeoPoint(38.7, -9.1),
  list: [1, 2, 3],
  object: { nested: true },
});

Firestore soporta string, number, boolean, null, Date, GeoPoint, arrays y objetos anidados. Cada documento tiene un límite de 1 MB.

CRUD Firestore


8 cards
Crear con setDoc
import { setDoc, doc } from "firebase/firestore";

await setDoc(doc(db, "users", "u1"), {
  name: "Ana",
  email: "ana@mail.com",
  age: 30,
});

setDoc() crea o sobrescribe un documento con el ID especificado. Si el documento ya existe, se sustituye por completo (a menos que uses merge: true).

Actualizar (updateDoc)
import { updateDoc, doc, increment } from "firebase/firestore";

await updateDoc(doc(db, "posts", "p1"), {
  title: "Título editado",
  likes: increment(1),
  editedAt: new Date(),
});

updateDoc() modifica solo los campos indicados sin afectar al resto. increment() suma atómicamente. Lanza error si el documento no existe.

Crear con addDoc (ID Automático)
import { addDoc, collection } from "firebase/firestore";

const ref = await addDoc(collection(db, "posts"), {
  title: "Nuevo post",
  content: "Texto aquí...",
  likes: 0,
  created: new Date(),
});

console.log("ID generado:", ref.id);

addDoc() genera automáticamente un ID único. Ideal cuando no necesitas controlar el identificador. Retorna la DocumentReference con el ref.id generado.

Merge (setDoc Parcial)
import { setDoc, doc } from "firebase/firestore";

// Solo actualiza los campos indicados, mantiene los demás
await setDoc(
  doc(db, "users", "u1"),
  { age: 31, city: "Lisbon" },
  { merge: true }
);

Con { merge: true }, el setDoc() hace update parcial en vez de sobrescribir. Crea el documento si no existe. Útil para updates sin conocer todos los campos.

Leer 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("Datos:", snap.data());
} else {
  console.log("El documento no existe");
}

getDoc() retorna un DocumentSnapshot. Usa snap.exists() para verificar si existe y snap.data() para obtener los campos como objeto.

Eliminar Documento (deleteDoc)
import { deleteDoc, doc } from "firebase/firestore";

await deleteDoc(doc(db, "users", "u1"));

// Eliminar campo específico (sin borrar el doc)
import { updateDoc, deleteField } from "firebase/firestore";

await updateDoc(doc(db, "users", "u2"), {
  oldField: deleteField(),
});

deleteDoc() elimina el documento entero. Para borrar solo un campo, usa deleteField() dentro de updateDoc(). Las subcolecciones no se eliminan automáticamente.

Leer Colección (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 un QuerySnapshot con todos los documentos. snap.size da el total y snap.forEach() itera cada documento.

Campos Especiales
import {
  serverTimestamp, arrayUnion,
  arrayRemove, increment
} from "firebase/firestore";

await updateDoc(ref, {
  updated: serverTimestamp(),
  tags: arrayUnion("javascript"),
  removed: arrayRemove("old"),
  views: increment(1),
});

serverTimestamp() usa la hora del servidor. arrayUnion() añade sin duplicar, arrayRemove() elimina e increment() suma atómicamente.

Consultas


8 cards
where (Filtros Básicos)
import { query, where, getDocs } from "firebase/firestore";

const q = query(
  collection(db, "users"),
  where("age", ">=", 18),
  where("active", "==", true)
);

const snap = await getDocs(q);

where() filtra documentos por campo. Puedes encadenar múltiples where() — funcionan como AND. El primer argumento es el campo, el segundo el operador.

Contar Documentos (count)
import { getCountFromServer, collection } from "firebase/firestore";

const snap = await getCountFromServer(collection(db, "users"));
console.log("Total:", snap.data().count);

// Con filtro
const q = query(collection(db, "posts"), where("public", "==", true));
const countSnap = await getCountFromServer(q);

getCountFromServer() cuenta documentos sin descargarlos — más barato en lecturas. Funciona con filtros. El resultado está en snap.data().count.

Operadores Disponibles
where("field", "==", value)
where("field", "!=", value)
where("field", ">", 10)
where("field", ">=", 10)
where("field", "<", 100)
where("field", "<=", 100)
where("field", "in", [1, 2, 3])
where("field", "not-in", [4, 5])
where("tags", "array-contains", "js")
where("tags", "array-contains-any", ["js", "ts"])

Operadores de comparación y de arrays. array-contains verifica si un array incluye el valor. in acepta hasta 30 valores. != excluye también documentos sin el campo.

Consultas a Subcolecciones
import { collectionGroup, query, where } from "firebase/firestore";

// Buscar en TODAS las subcolecciones "comments"
const q = query(
  collectionGroup(db, "comments"),
  where("author", "==", "ana@mail.com")
);

const snap = await getDocs(q);

collectionGroup() búsqueda en todas las subcolecciones con el mismo nombre, en cualquier documento. Requiere un índice especial activado en la consola.

Ordenación (orderBy)
import { query, orderBy, getDocs } from "firebase/firestore";

const q = query(
  collection(db, "posts"),
  orderBy("created", "desc"),
  orderBy("title", "asc")
);

const snap = await getDocs(q);

orderBy() ordena por un campo. El segundo argumento es "asc" (por defecto) o "desc". Puedes encadenar para ordenación secundaria.

Limitaciones de Consultas
// ❌ No puedes combinar != con orderBy en otro campo
// ❌ Máx. 1 filtro de desigualdad por query
// ❌ in / array-contains-any: máx. 30 valores
// ❌ orderBy + where en campos diferentes → índice compuesto

// ✅ Solución: crear índice compuesto en la consola
// o dividir en dos consultas en el cliente

Firestore tiene limitaciones: solo un operador de desigualdad por consulta, y orderBy en un campo diferente del where exige índice compuesto configurado en la consola.

Paginación (limit y startAfter)
import { query, orderBy, limit, startAfter } from "firebase/firestore";

// Primera página
const q1 = query(collection(db, "posts"), orderBy("created"), limit(10));
const snap1 = await getDocs(q1);
const last = snap1.docs[snap1.docs.length - 1];

// Página siguiente
const q2 = query(collection(db, "posts"), orderBy("created"), startAfter(last), limit(10));

limit() restringe el número de resultados. startAfter() recibe el último documento de la página anterior como cursor para paginación eficiente.

startAt y endAt (Intervalos)
import { query, orderBy, startAt, endAt } from "firebase/firestore";

// Documentos entre A y M
const q = query(
  collection(db, "users"),
  orderBy("name"),
  startAt("A"),
  endAt("M\uf8ff")
);

const snap = await getDocs(q);

startAt() y endAt() definen límites inclusivos. startAfter() y endBefore() son exclusivos. El carácter \uf8ff garantiza la inclusión total del prefijo.

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("Datos actuales:", snap.data());
  }
});

// Dejar de escuchar
unsub();

onSnapshot() crea un listener en tiempo real. Se ejecuta inmediatamente con los datos actuales y luego en cada cambio. Retorna una función unsub() para cancelar.

Metadata (Origen de los Datos)
onSnapshot(ref, (snap) => {
  const source = snap.metadata.fromCache ? "caché local" : "servidor";
  const pending = snap.metadata.hasPendingWrites;
  console.log(`Fuente: ${source} | Escritura pendiente: ${pending}`);
});

snap.metadata.fromCache indica si los datos vinieron de la caché offline. hasPendingWrites muestra si hay escrituras locales aún no sincronizadas con el servidor.

onSnapshot (Colección)
const unsub = onSnapshot(collection(db, "messages"), (snapshot) => {
  snapshot.docChanges().forEach((change) => {
    if (change.type === "added") {
      console.log("Nueva:", change.doc.data());
    }
    if (change.type === "modified") {
      console.log("Editada:", change.doc.data());
    }
    if (change.type === "removed") {
      console.log("Eliminada:", change.doc.id);
    }
  });
});

En colecciones, snapshot.docChanges() muestra exactamente qué cambió: "added", "modified" o "removed". Más eficiente que re-renderizar todo.

Include Metadata Changes
// Por defecto, no se dispara cuando solo cambia la metadata
// Para recibir también cambios de metadata:
const unsub = onSnapshot(ref, {
  includeMetadataChanges: true,
}, (snap) => {
  // Se dispara cuando cambian datos O metadata
  console.log("fromCache:", snap.metadata.fromCache);
});

Con includeMetadataChanges: true, el listener se dispara también cuando el estado de sincronización cambia (ej.: de caché a servidor), no solo cuando cambian los datos.

Listener con Query
import { query, where, orderBy, onSnapshot } from "firebase/firestore";

const q = query(
  collection(db, "messages"),
  where("room", "==", "general"),
  orderBy("created", "desc")
);

const unsub = onSnapshot(q, (snap) => {
  const msgs = snap.docs.map(d => ({ id: d.id, ...d.data() }));
  render(msgs);
});

Puedes pasar una query al onSnapshot() en vez de una colección. El listener solo recibe documentos que corresponden al filtro.

Desconectar Listeners
// Guardar referencia para limpiar después
const listeners = [];

listeners.push(onSnapshot(ref1, cb1));
listeners.push(onSnapshot(ref2, cb2));

// Limpiar todos (ej.: onUnmount en React)
function cleanup() {
  listeners.forEach((unsub) => unsub());
  listeners.length = 0;
}

// React: useEffect(() => { return unsub; }, []);

Cada onSnapshot() cuenta como una lectura activa. Siempre que el componente se destruye, llama a unsub() para evitar memory leaks y lecturas innecesarias.

Tratamiento de Errores
const unsub = onSnapshot(ref, {
  next: (snap) => {
    console.log("Datos:", snap.data());
  },
  error: (err) => {
    console.error("Error en el listener:", err.code, err.message);
    // permission-denied, unavailable, etc.
  },
});

Pasa un objeto con next y error en vez de un callback simple. Errores comunes: permission-denied (reglas) y unavailable (red).

Offline y Persistencia
import { initializeFirestore, persistentLocalCache } from "firebase/firestore";

// Activar caché persistente (IndexedDB)
const db = initializeFirestore(app, {
  localCache: persistentLocalCache({ tabManager: persistentMultipleTabManager() }),
});

// Escritura offline → sincroniza cuando vuelva la red
await setDoc(doc(db, "posts", "p1"), { title: "Offline" });

Con persistentLocalCache(), los datos sobreviven a los reloads. Las escrituras offline se guardan localmente y se sincronizan automáticamente cuando vuelve la conexión.

Autenticação


8 cards
Registrarse (Email/Contraseña)
import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";

const auth = getAuth();
const cred = await createUserWithEmailAndPassword(auth, email, password);
console.log("UID:", cred.user.uid);

createUserWithEmailAndPassword() crea la cuenta y hace login automático. Retorna UserCredential con cred.user.uid. La contraseña debe tener 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("Nombre:", cred.user.displayName);
console.log("Foto:", cred.user.photoURL);

GoogleAuthProvider + signInWithPopup() abre un popup de login de Google. addScope() pide permisos extra. También disponible: signInWithRedirect() para móvil.

Login (Email/Contraseña)
import { signInWithEmailAndPassword } from "firebase/auth";

try {
  const cred = await signInWithEmailAndPassword(auth, email, password);
  console.log("Bienvenido:", cred.user.email);
} catch (err) {
  // auth/invalid-credential (Firebase v10+)
  console.error("Error:", err.code);
}

signInWithEmailAndPassword() autentica al usuario. Errores comunes: auth/invalid-credential, auth/user-not-found, auth/wrong-password.

Reset de Contraseña
import { sendPasswordResetEmail } from "firebase/auth";

await sendPasswordResetEmail(auth, "user@mail.com");
// Email de reset enviado

// Tras el login, cambiar contraseña:
import { updatePassword } from "firebase/auth";
await updatePassword(auth.currentUser, "newPassword123");

sendPasswordResetEmail() envía un email de recuperación. updatePassword() cambia la contraseña del usuario conectado — requiere sesión reciente (si no, pide re-auth).

Estado de Autenticación
import { onAuthStateChanged } from "firebase/auth";

const unsub = onAuthStateChanged(auth, (user) => {
  if (user) {
    console.log("Conectado:", user.uid, user.email);
    console.log("Token:", user.accessToken);
  } else {
    console.log("Sesión terminada");
  }
});

onAuthStateChanged() es el listener principal de auth. Se dispara al cargar la página y en cada login/logout. Esencial para proteger rutas y mostrar UI condicional.

Datos del Perfil
import { updateProfile } from "firebase/auth";

await updateProfile(auth.currentUser, {
  displayName: "Ana Silva",
  photoURL: "https://example.com/photo.jpg",
});

// Recargar datos
await auth.currentUser.reload();
console.log(auth.currentUser.displayName);

updateProfile() cambia displayName y photoURL. Para datos personalizados (edad, bio), guárdalos en Firestore en un documento con el mismo uid.

Logout
import { signOut } from "firebase/auth";

await signOut(auth);
// onAuthStateChanged se dispara con user = null

signOut() termina la sesión. El listener onAuthStateChanged() es notificado automáticamente con user = null. Limpia el estado de la app en ese callback.

Tokens y Verificación
// Obtener token ID (JWT) para enviar a APIs
const token = await auth.currentUser.getIdToken(true);

// En el 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 un JWT para autenticar peticiones a APIs. En el backend, admin.auth().verifyIdToken() valida el token y extrae el uid.

Storage


8 cards
Subida de Archivo
import { getStorage, ref, uploadBytes } from "firebase/storage";

const storage = getStorage();
const fileRef = ref(storage, "photos/image.jpg");

const result = await uploadBytes(fileRef, file);
console.log("Path:", result.ref.fullPath);

uploadBytes() envía el archivo de una vez. El primer argumento es la ref() con la ruta en el bucket, el segundo es el File o Blob.

Listar Archivos
import { listAll, ref } from "firebase/storage";

const result = await listAll(ref(storage, "photos/"));

result.items.forEach((item) => {
  console.log("Archivo:", item.fullPath);
});

result.prefixes.forEach((folder) => {
  console.log("Subcarpeta:", folder.fullPath);
});

listAll() retorna items (archivos) y prefixes (subcarpetas). Para muchas carpetas, usa list() con paginación vía pageToken.

Obtener URL de Descarga
import { getDownloadURL, ref } from "firebase/storage";

const url = await getDownloadURL(ref(storage, "photos/image.jpg"));

// Usar en HTML
imgElement.src = url;

// O guardarlo en Firestore
await updateDoc(docRef, { photoURL: url });

getDownloadURL() retorna una URL temporal con token de acceso. Guárdala en Firestore para referencia futura. La URL cambia si el archivo es sustituido.

Eliminar Archivo
import { deleteObject, ref } from "firebase/storage";

await deleteObject(ref(storage, "photos/image.jpg"));

// Eliminar múltiples
const paths = ["photos/a.jpg", "photos/b.jpg"];
await Promise.all(
  paths.map((p) => deleteObject(ref(storage, p)))
);

deleteObject() elimina un archivo del bucket. Lanza el error object-not-found si no existe. Usa Promise.all() para eliminar varios en paralelo.

Subida con Progreso
import { uploadBytesResumable } from "firebase/storage";

const task = uploadBytesResumable(fileRef, file);

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 el progreso y pausar/reanudar. state_changed se dispara con cada chunk enviado. Ideal para archivos grandes.

Metadata del Archivo
import { getMetadata, updateMetadata } from "firebase/storage";

const meta = await getMetadata(fileRef);
console.log(meta.contentType, meta.size, meta.timeCreated);

// Actualizar metadata
await updateMetadata(fileRef, {
  contentType: "image/webp",
  customMetadata: { author: "ana" },
});

getMetadata() retorna contentType, size, timeCreated. updateMetadata() permite cambiar el tipo MIME y añadir customMetadata.

Pausar y Reanudar Subida
const task = uploadBytesResumable(fileRef, file);

// Pausar
task.pause();

// Reanudar
task.resume();

// Cancelar
task.cancel();

El objeto UploadTask tiene métodos pause(), resume() y cancel(). Útil para conexiones lentas o cuando el usuario navega a otra página.

Subida 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, "notes.txt"), "Contenido aquí", "raw", {
  contentType: "text/plain",
});

uploadString() envía strings directamente. Formatos: "raw", "base64", "base64url" y "data_url". Útil para imágenes generadas por canvas.

Regras de Segurança


8 cards
Estructura 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;
    }
  }
}

Las reglas usan match para definir rutas y allow para permisos. request.auth.uid es el usuario autenticado. Sin regla = acceso denegado.

Reglas en Subcolecciones
match /posts/{postId} {
  allow read: if true;

  match /comments/{commentId} {
    allow read: if true;
    allow create: if request.auth != null
      && request.resource.data.postId == postId;
  }
}

Las subcolecciones heredan el contexto del match padre. La variable {postId} queda accesible en las reglas de la subcolección para validaciones cruzadas.

Operaciones Granulares
match /posts/{postId} {
  allow get: if true;           // leer 1 doc
  allow list: if true;          // listar colección
  allow create: if request.auth != null;
  allow update: if request.auth.uid == resource.data.authorId;
  allow delete: if false;       // nadie borra
}

read = get + list. write = create + update + delete. Usar operaciones granulares da un control más fino sobre el acceso.

Funciones Personalizadas
function isAdmin() {
  return request.auth != null
    && request.auth.token.admin == true;
}

function isDocOwner(userId) {
  return request.auth.uid == userId;
}

match /users/{userId} {
  allow read: if isAdmin() || isDocOwner(userId);
  allow write: if isDocOwner(userId);
}

function crea helpers reutilizables. Reduce la repetición y hace las reglas más legibles. Pueden recibir parámetros y acceder a request y resource.

request vs resource
allow update: if
  // Datos que el cliente quiere grabar
  request.resource.data.title is string
  && request.resource.data.title.size() > 0
  // Datos actuales en el servidor
  && resource.data.authorId == request.auth.uid;

request.resource.data son los datos nuevos (a grabar). resource.data son los datos actuales en el servidor. Compara ambos para validar cambios.

get() para Verificar Otros Docs
match /posts/{postId} {
  allow create: if
    request.auth != null
    && get(/databases/$(database)/documents/users/$(request.auth.uid))
       .data.plan == "premium";
}

get() lee otro documento durante la evaluación de la regla. Útil para verificar perfiles, suscripciones o permisos guardados en otro lugar. Tiene coste de lectura.

Validar Tipos de Datos
allow create: if
  request.resource.data.name is string
  && request.resource.data.age is int
  && request.resource.data.age >= 0
  && request.resource.data.email is string
  && request.resource.data.email.matches(".*@.*\..*")
  && request.resource.data.keys().hasAll(["name", "email"]);

Valida tipos con is string, is int, is bool. matches() aplica regex. keys().hasAll() garantiza campos obligatorios.

Reglas de Storage
service firebase.storage {
  match /b/{bucket}/o {
    match /photos/{userId}/{fileName} {
      allow read: if true;
      allow write: if request.auth.uid == userId
        && request.resource.size < 5 * 1024 * 1024
        && request.resource.contentType.matches("image/.*");
    }
  }
}

Las reglas de Storage usan request.resource.size (bytes) y request.resource.contentType. Limita el tamaño y tipo de archivo por seguridad.

Avançado


8 cards
Batch Writes
import { writeBatch, doc } from "firebase/firestore";

const batch = writeBatch(db);
batch.set(doc(db, "users", "u1"), { name: "Ana" });
batch.update(doc(db, "users", "u2"), { active: false });
batch.delete(doc(db, "temp", "x"));

await batch.commit(); // todo o nada

writeBatch() agrupa hasta 500 operaciones (set, update, delete) en una única escritura atómica. O todas tienen éxito o ninguna se aplica. No permite lecturas.

Índices Compuestos
// firestore.indexes.json
{
  "indexes": [{
    "collectionGroup": "posts",
    "queryScope": "COLLECTION",
    "fields": [
      { "fieldPath": "author", "order": "ASCENDING" },
      { "fieldPath": "created", "order": "DESCENDING" }
    ]
  }]
}

Las consultas con where + orderBy en campos diferentes exigen índices compuestos. Defínelos en firestore.indexes.json y publica con firebase deploy.

Transacciones (runTransaction)
import { runTransaction, doc } from "firebase/firestore";

await runTransaction(db, async (tx) => {
  const snap = await tx.get(doc(db, "accounts", "c1"));
  if (!snap.exists()) throw "La cuenta no existe";

  const newBalance = snap.data().balance - 50;
  if (newBalance < 0) throw "Saldo insuficiente";

  tx.update(doc(db, "accounts", "c1"), { balance: newBalance });
});

runTransaction() permite lectura + escritura atómica. Si los datos cambian durante la ejecución, Firestore reintenta automáticamente (hasta 5 intentos).

Emuladores Locales
// firebase.json
{
  "emulators": {
    "firestore": { "port": 8080 },
    "auth": { "port": 9099 },
    "storage": { "port": 9199 },
    "ui": { "enabled": true, "port": 4000 }
  }
}

// Conectar el SDK al emulador
import { connectFirestoreEmulator } from "firebase/firestore";
connectFirestoreEmulator(db, "localhost", 8080);

Los emuladores simulan Firestore, Auth y Storage localmente sin costes. connectFirestoreEmulator() redirige el SDK. UI de debug en localhost:4000.

Cloud Functions (Triggers)
// functions/index.js
const { onDocumentCreated } = require("firebase-functions/v2/firestore");

exports.onNewUser = onDocumentCreated("users/{userId}", (event) => {
  const data = event.data.data();
  console.log("Nuevo user:", data.email);
  // Enviar email de bienvenida, crear doc de perfil, etc.
});

Cloud Functions ejecuta código en el servidor en respuesta a eventos. Triggers: onDocumentCreated, onDocumentUpdated, onDocumentDeleted, onDocumentWritten.

Seguridad (Buenas Prácticas)
// ❌ NUNCA en producción:
allow read, write: if true;

// ✅ Siempre autenticar:
allow read: if request.auth != null;

// ✅ Validar datos de entrada:
allow create: if request.resource.data.keys().hasAll(["name"]);

// ✅ Limitar por usuario:
allow write: if request.auth.uid == userId;

Nunca uses allow read, write: if true en producción. Autentica siempre con request.auth, valida los datos de entrada y limita el acceso por uid.

Admin SDK (Servidor)
const admin = require("firebase-admin");
admin.initializeApp();

const db = admin.firestore();

// Crear
await db.collection("users").doc("u1").set({ name: "Ana" });

// Query
const snap = await db.collection("users").where("active", "==", true).get();

// Eliminar una colección entera
const docs = await db.collection("temp").listDocuments();
await Promise.all(docs.map(d => d.delete()));

El Admin SDK corre en el servidor sin reglas de seguridad. Usa admin.firestore() en vez de getFirestore(). Ideal para tareas administrativas y migraciones.

Límites y Cuotas
// Límites principales de Firestore:
// - Documento: máx. 1 MB
// - Profundidad de subcolecciones: 100 niveles
// - Batch: máx. 500 operaciones
// - Campos por documento: sin límite práctico
// - Escritura: ~10k writes/seg por BD
// - Lectura gratis: 50k/día (plan Spark)

Conoce los límites: documentos hasta 1 MB, batch hasta 500 ops, subcolecciones hasta 100 niveles. El plan gratuito (Spark) da 50k lecturas y 20k escrituras por día.