DevTools

Cheatsheet Firebase / Firestore

Plataforma NoSQL da Google para apps em tempo real e cloud

Back to languages
Firebase / Firestore
64 cards found
Categories:
Versions:

Setup and Configuration


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

// Import only what you need (tree-shaking)
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";

The modular v9+ SDK enables tree-shaking — it only includes what you use in the bundle. Install with npm install firebase and import specific functions from each module.

References (collection and doc)
import { collection, doc } from "firebase/firestore";

// Collection reference
const usersRef = collection(db, "users");

// Reference to a specific document
const userRef = doc(db, "users", "user123");

// Subcollection reference
const postsRef = collection(db, "users/user123/posts");

collection() points to a collection and doc() to a document. They are just references — no read happens until you call getDoc() or getDocs().

Initialize 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);

The firebaseConfig object comes from the Firebase console. initializeApp() creates the instance and getFirestore() returns the database reference.

Multiple 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);

You can have multiple instances by passing a name the the second argument of initializeApp(). Useful for connecting to different Firebase projects in the same app.

Firebase CLI
npm install -g firebase-tools
firebase login
firebase init          // project setup
firebase deploy        // publish rules/functions
firebase emulators:start  // local emulators

firebase-tools is the official CLI. firebase init configures the project locally and firebase deploy publishes rules, functions and hosting.

Environment Variables
// .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,
};

Never hardcode credentials. Use environment variables with the VITE_ prefix (Vite) or NEXT_PUBLIC_ (Next.js) to expose them to the client safely.

Data Structure
// Firestore: Collections → Documents → Fields
users/              // collection
  user123/          // document (ID: user123)
    name: "Anna"     // field
    age: 30
    posts/          // subcollection
      post1/
        title: "Hello"

Firestore is a document-oriented NoSQL database. Data is organized into collections that contain documents, which can have nested subcollections.

Supported Data Types
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 supports string, number, boolean, null, Date, GeoPoint, arrays and nested objects. Each document has a 1 MB limit.

CRUD Firestore


8 cards
Create with setDoc
import { setDoc, doc } from "firebase/firestore";

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

setDoc() creates or overwrites a document with the specified ID. If the document already exists, it is completely replaced (unless you use merge: true).

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

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

updateDoc() modifies only the specified fields without affecting the rest. increment() adds atomically. Throws an error if the document does not exist.

Create with addDoc (Auto ID)
import { addDoc, collection } from "firebase/firestore";

const ref = await addDoc(collection(db, "posts"), {
  title: "New post",
  content: "Text here...",
  likes: 0,
  created: new Date(),
});

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

addDoc() automatically generates a unique ID. Ideal when you don't need to control the identifier. Returns the DocumentReference with the generated ref.id.

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

// Only updates the given fields, keeps the others
await setDoc(
  doc(db, "users", "u1"),
  { age: 31, city: "Lisbon" },
  { merge: true }
);

With { merge: true }, setDoc() does a partial update instead of overwriting. Creates the document if it does not exist. Useful for updates without knowing all the fields.

Read Document (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("Data:", snap.data());
} else {
  console.log("Document does not exist");
}

getDoc() returns a DocumentSnapshot. Use snap.exists() to check whether it exists and snap.data() to get the fields the an object.

Delete Document (deleteDoc)
import { deleteDoc, doc } from "firebase/firestore";

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

// Delete a specific field (without removing the doc)
import { updateDoc, deleteField } from "firebase/firestore";

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

deleteDoc() removes the whole document. To delete just one field, use deleteField() inside updateDoc(). Subcollections are not deleted automatically.

Read Collection (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() returns a QuerySnapshot with all the documents. snap.size gives the total and snap.forEach() iterates over each document.

Special Fields
import {
  serverTimestamp, arrayUnion,
  arrayRemove, increment
} from "firebase/firestore";

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

serverTimestamp() uses the server time. arrayUnion() adds without duplicating, arrayRemove() removes and increment() adds atomically.

Queries


8 cards
where (Basic Filters)
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() filters documents by field. You can chain multiple where() — they work the AND. The first argument is the field, the second the operator.

Count Documents (count)
import { getCountFromServer, collection } from "firebase/firestore";

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

// With a filter
const q = query(collection(db, "posts"), where("public", "==", true));
const countSnap = await getCountFromServer(q);

getCountFromServer() counts documents without downloading them — cheaper in reads. Works with filters. The result is in snap.data().count.

Available Operators
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"])

Comparison and array operators. array-contains checks whether an array includes the value. in accepts up to 30 values. != also excludes documents without the field.

Subcollection Queries
import { collectionGroup, query, where } from "firebase/firestore";

// Search in ALL "comments" subcollections
const q = query(
  collectionGroup(db, "comments"),
  where("author", "==", "ana@mail.com")
);

const snap = await getDocs(q);

collectionGroup() searches across all subcollections with the same name, in any document. Requires a special index enabled in the console.

Sorting (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() sorts by a field. The second argument is "asc" (default) or "desc". You can chain them for secondary sorting.

Query Limitations
// ❌ You cannot combine != with orderBy on another field
// ❌ Max. 1 inequality filter per query
// ❌ in / array-contains-any: max. 30 values
// ❌ orderBy + where on different fields → composite index

// ✅ Solution: create a composite index in the console
// or split into two queries on the client

Firestore has limitations: only one inequality operator per query, and orderBy on a field different from the where requires a composite index configured in the console.

Pagination (limit and startAfter)
import { query, orderBy, limit, startAfter } from "firebase/firestore";

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

// Next page
const q2 = query(collection(db, "posts"), orderBy("created"), startAfter(last), limit(10));

limit() restricts the number of results. startAfter() receives the last document of the previous page the a cursor for efficient pagination.

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

// Documents between A and M
const q = query(
  collection(db, "users"),
  orderBy("name"),
  startAt("A"),
  endAt("M\uf8ff")
);

const snap = await getDocs(q);

startAt() and endAt() define inclusive bounds. startAfter() and endBefore() are exclusive. The \uf8ff character guarantees full prefix inclusion.

Real Time


8 cards
onSnapshot (Document)
import { onSnapshot, doc } from "firebase/firestore";

const unsub = onSnapshot(doc(db, "users", "u1"), (snap) => {
  if (snap.exists()) {
    console.log("Current data:", snap.data());
  }
});

// Stop listening
unsub();

onSnapshot() creates a real-time listener. It runs immediately with the current data and then on every change. Returns an unsub() function to cancel.

Metadata (Data Source)
onSnapshot(ref, (snap) => {
  const source = snap.metadata.fromCache ? "local cache" : "server";
  const pending = snap.metadata.hasPendingWrites;
  console.log(`Source: ${source} | Pending write: ${pending}`);
});

snap.metadata.fromCache indicates whether the data came from the offline cache. hasPendingWrites shows whether there are local writes not yet synced to the server.

onSnapshot (Collection)
const unsub = onSnapshot(collection(db, "messages"), (snapshot) => {
  snapshot.docChanges().forEach((change) => {
    if (change.type === "added") {
      console.log("New:", change.doc.data());
    }
    if (change.type === "modified") {
      console.log("Edited:", change.doc.data());
    }
    if (change.type === "removed") {
      console.log("Removed:", change.doc.id);
    }
  });
});

On collections, snapshot.docChanges() shows exactly what changed: "added", "modified" or "removed". More efficient than re-rendering everything.

Include Metadata Changes
// By default, it does not fire when only metadata changes
// To also receive metadata changes:
const unsub = onSnapshot(ref, {
  includeMetadataChanges: true,
}, (snap) => {
  // Fires when data OR metadata change
  console.log("fromCache:", snap.metadata.fromCache);
});

With includeMetadataChanges: true, the listener also fires when the sync state changes (e.g. from cache to server), not only when the data changes.

Listener with 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);
});

You can pass a query to onSnapshot() instead of a collection. The listener only receives documents that match the filter.

Detach Listeners
// Keep a reference to clean up later
const listeners = [];

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

// Clean up all (e.g. onUnmount in React)
function cleanup() {
  listeners.forEach((unsub) => unsub());
  listeners.length = 0;
}

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

Each onSnapshot() counts the an active read. Whenever the component is destroyed, call unsub() to avoid memory leaks and unnecessary reads.

Error Handling
const unsub = onSnapshot(ref, {
  next: (snap) => {
    console.log("Data:", snap.data());
  },
  error: (err) => {
    console.error("Listener error:", err.code, err.message);
    // permission-denied, unavailable, etc.
  },
});

Pass an object with next and error instead of a simple callback. Common errors: permission-denied (rules) and unavailable (network).

Offline and Persistence
import { initializeFirestore, persistentLocalCache } from "firebase/firestore";

// Enable persistent cache (IndexedDB)
const db = initializeFirestore(app, {
  localCache: persistentLocalCache({ tabManager: persistentMultipleTabManager() }),
});

// Offline write → syncs when the network is back
await setDoc(doc(db, "posts", "p1"), { title: "Offline" });

With persistentLocalCache(), data survives reloads. Offline writes are stored locally and synced automatically when the connection comes back.

Authentication


8 cards
Sign Up (Email/Password)
import { getAuth, createUserWithEmailAndPassword } from "firebase/auth";

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

createUserWithEmailAndPassword() creates the account and signs in automatically. Returns a UserCredential with cred.user.uid. The password must be at least 6 characters.

Social Login (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("Name:", cred.user.displayName);
console.log("Photo:", cred.user.photoURL);

GoogleAuthProvider + signInWithPopup() opens a Google login popup. addScope() requests extra permissions. Also available: signInWithRedirect() for mobile.

Login (Email/Password)
import { signInWithEmailAndPassword } from "firebase/auth";

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

signInWithEmailAndPassword() authenticates the user. Common errors: auth/invalid-credential, auth/user-not-found, auth/wrong-password.

Password Reset
import { sendPasswordResetEmail } from "firebase/auth";

await sendPasswordResetEmail(auth, "user@mail.com");
// Reset email sent

// After login, change password:
import { updatePassword } from "firebase/auth";
await updatePassword(auth.currentUser, "newPassword123");

sendPasswordResetEmail() sends a recovery email. updatePassword() changes the logged-in user's password — requires a recent session (otherwise it asks to re-auth).

Authentication State
import { onAuthStateChanged } from "firebase/auth";

const unsub = onAuthStateChanged(auth, (user) => {
  if (user) {
    console.log("Logged in:", user.uid, user.email);
    console.log("Token:", user.accessToken);
  } else {
    console.log("Session ended");
  }
});

onAuthStateChanged() is the main auth listener. It fires on page load and on every login/logout. Essential for protecting routes and showing conditional UI.

Profile Data
import { updateProfile } from "firebase/auth";

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

// Reload data
await auth.currentUser.reload();
console.log(auth.currentUser.displayName);

updateProfile() changes displayName and photoURL. For custom data (age, bio), store it in Firestore in a document with the same uid.

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

await signOut(auth);
// onAuthStateChanged fires with user = null

signOut() ends the session. The onAuthStateChanged() listener is notified automatically with user = null. Clear the app state in that callback.

Tokens and Verification
// Get the ID token (JWT) to send to APIs
const token = await auth.currentUser.getIdToken(true);

// On the server (Node.js Admin SDK):
const admin = require("firebase-admin");
const decoded = await admin.auth().verifyIdToken(token);
console.log("Verified UID:", decoded.uid);

getIdToken() returns a JWT to authenticate requests to APIs. On the backend, admin.auth().verifyIdToken() validates the token and extracts the uid.

Storage


8 cards
File Upload
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() uploads the file all at once. The first argument is the ref() with the path in the bucket, the second is the File or Blob.

List Files
import { listAll, ref } from "firebase/storage";

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

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

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

listAll() returns items (files) and prefixes (subfolders). For many folders, use list() with pagination via pageToken.

Get Download URL
import { getDownloadURL, ref } from "firebase/storage";

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

// Use in HTML
imgElement.src = url;

// Or store it in Firestore
await updateDoc(docRef, { photoURL: url });

getDownloadURL() returns a temporary URL with an access token. Store it in Firestore for future reference. The URL changes if the file is replaced.

Delete File
import { deleteObject, ref } from "firebase/storage";

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

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

deleteObject() removes a file from the bucket. Throws an object-not-found error if it does not exist. Use Promise.all() to delete several in parallel.

Upload with Progress
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 complete!"),
});

uploadBytesResumable() lets you monitor progress and pause/resume. state_changed fires on every chunk sent. Ideal for large files.

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

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

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

getMetadata() returns contentType, size, timeCreated. updateMetadata() lets you change the MIME type and add customMetadata.

Pause and Resume Upload
const task = uploadBytesResumable(fileRef, file);

// Pause
task.pause();

// Resume
task.resume();

// Cancel
task.cancel();

The UploadTask object has pause(), resume() and cancel() methods. Useful for slow connections or when the user navigates to another page.

String/Base64 Upload
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"), "Content here", "raw", {
  contentType: "text/plain",
});

uploadString() uploads strings directly. Formats: "raw", "base64", "base64url" and "data_url". Useful for canvas-generated images.

Security Rules


8 cards
Basic Structure
rules_version = "2";
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read: if true;
      allow write: if request.auth.uid == userId;
    }
  }
}

Rules use match to define paths and allow for permissions. request.auth.uid is the authenticated user. No rule = access denied.

Rules in Subcollections
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;
  }
}

Subcollections inherit the context of the parent match. The {postId} variable stays accessible in the subcollection rules for cross validations.

Granular Operations
match /posts/{postId} {
  allow get: if true;           // read 1 doc
  allow list: if true;          // list collection
  allow create: if request.auth != null;
  allow update: if request.auth.uid == resource.data.authorId;
  allow delete: if false;       // nobody deletes
}

read = get + list. write = create + update + delete. Using granular operations gives finer control over access.

Custom Functions
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 creates reusable helpers. It reduces repetition and makes the rules more readable. They can take parameters and access request and resource.

request vs resource
allow update: if
  // Data the client wants to write
  request.resource.data.title is string
  && request.resource.data.title.size() > 0
  // Current data on the server
  && resource.data.authorId == request.auth.uid;

request.resource.data is the new data (to be written). resource.data is the current data on the server. Compare both to validate changes.

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

get() reads another document during rule evaluation. Useful to check profiles, subscriptions or permissions stored elsewhere. It has a read cost.

Validate Data Types
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"]);

Validate types with is string, is int, is bool. matches() applies a regex. keys().hasAll() guarantees required fields.

Storage Rules
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/.*");
    }
  }
}

Storage rules use request.resource.size (bytes) and request.resource.contentType. Limit file size and type for security.

Advanced


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

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

await batch.commit(); // all or nothing

writeBatch() groups up to 500 operations (set, update, delete) into a single atomic write. Either all succeed or none is applied. It does not allow reads.

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

Queries with where + orderBy on different fields require composite indexes. Define them in firestore.indexes.json and publish with firebase deploy.

Transactions (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 "Account does not exist";

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

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

runTransaction() allows atomic read + write. If the data changes during execution, Firestore retries automatically (up to 5 attempts).

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

// Connect the SDK to the emulator
import { connectFirestoreEmulator } from "firebase/firestore";
connectFirestoreEmulator(db, "localhost", 8080);

The emulators simulate Firestore, Auth and Storage locally at no cost. connectFirestoreEmulator() redirects the SDK. Debug UI at 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("New user:", data.email);
  // Send welcome email, create profile doc, etc.
});

Cloud Functions runs code on the server in response to events. Triggers: onDocumentCreated, onDocumentUpdated, onDocumentDeleted, onDocumentWritten.

Security (Best Practices)
// ❌ NEVER in production:
allow read, write: if true;

// ✅ Always authenticate:
allow read: if request.auth != null;

// ✅ Validate input data:
allow create: if request.resource.data.keys().hasAll(["name"]);

// ✅ Restrict per user:
allow write: if request.auth.uid == userId;

Never use allow read, write: if true in production. Always authenticate with request.auth, validate input data and restrict access by uid.

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

const db = admin.firestore();

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

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

// Delete an entire collection
const docs = await db.collection("temp").listDocuments();
await Promise.all(docs.map(d => d.delete()));

The Admin SDK runs on the server without security rules. Use admin.firestore() instead of getFirestore(). Ideal for administrative tasks and migrations.

Limits and Quotas
// Main Firestore limits:
// - Document: max. 1 MB
// - Subcollection depth: 100 levels
// - Batch: max. 500 operations
// - Fields per document: no practical limit
// - Writes: ~10k writes/sec per DB
// - Free reads: 50k/day (Spark plan)

Know the limits: documents up to 1 MB, batches up to 500 ops, subcollections up to 100 levels. The free plan (Spark) gives 50k reads and 20k writes per day.