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 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
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
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
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
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 = nullsignOut() 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
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
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
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 nadawriteBatch() 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.