Start hereQuick start · 6 you’ll reach for daily
Target versions · paceVersions
firebase (JS SDK) ≥ 10.12
firebase-admin ≥ 12
firebase-functions ≥ 5 (gen2)
firebase-tools ≥ 13
The modern JS SDK is modular and tree-shakable — always import from
firebase/firestore, firebase/auth,
etc., not the deprecated compat namespace. Cloud Functions gen 2 runs on Cloud Run under the hood:
cold-starts are quicker, concurrency is configurable, and secrets are first-class. The Realtime Database
still exists, but Firestore is the default for new projects.
CLI · emulators · SDKSetup
# CLI
npm install -g firebase-tools
firebase --version
firebase login
# Pick products inside the wizard:
# firestore, functions, hosting, storage, emulators
firebase init
# Local stack — auth, firestore, functions, storage, hosting on one box
firebase emulators:start --import=./seed --export-on-exit=./seed
# Emulator UI: http://localhost:4000
# Web SDK (modular, tree-shakable)
npm i firebase
# Initialise once per app
cat > src/firebase.ts <<'TS'
import { initializeApp } from 'firebase/app'
import { getAuth, connectAuthEmulator } from 'firebase/auth'
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore'
import { getStorage, connectStorageEmulator } from 'firebase/storage'
const app = initializeApp({
apiKey: import.meta.env.VITE_FB_API_KEY,
authDomain: import.meta.env.VITE_FB_AUTH_DOMAIN,
projectId: import.meta.env.VITE_FB_PROJECT_ID,
storageBucket: import.meta.env.VITE_FB_STORAGE_BUCKET,
appId: import.meta.env.VITE_FB_APP_ID,
})
export const auth = getAuth(app)
export const db = getFirestore(app)
export const storage = getStorage(app)
if (import.meta.env.DEV) {
connectAuthEmulator(auth, 'http://localhost:9099')
connectFirestoreEmulator(db, 'localhost', 8080)
connectStorageEmulator(storage, 'localhost', 9199)
}
TS
firebase ...Firebase CLI
| npm install -g firebase-tools | Install. Self-updating on use. |
| firebase login | OAuth in the browser; stores creds in ~/.config/configstore. |
| firebase init | Interactive wizard — pick products: firestore, functions, hosting, storage, emulators. |
| firebase use --add | Add a project alias (e.g. dev / prod). |
| firebase emulators:start --import=./seed --export-on-exit=./seed | Local stack with deterministic seed data. UI on :4000. |
| firebase deploy --only firestore:rules,firestore:indexes | Deploy just the rules + indexes — safer than a full deploy. |
| firebase deploy --only functions:api,functions:onNewNote | Selective function deploy. |
| firebase functions:log [--only api] | Stream logs. |
| firebase firestore:delete /col --recursive | Bulk delete a collection / subtree. |
| firebase apps:create web my-app | Provision a Web app + print the config object. |
Documents & collectionsFirestore basics
| import { getFirestore, collection, doc, addDoc, setDoc, getDoc, updateDoc, deleteDoc, serverTimestamp } from 'firebase/firestore' | Modular imports. |
| const db = getFirestore(app) | One Firestore instance per app. |
| addDoc(collection(db, 'notes'), { ... }) | Server-assigned id. Returns a DocumentReference. |
| setDoc(doc(db,'notes', id), {...}, { merge: true }) | Upsert. merge: true preserves omitted fields. |
| getDoc(doc(db,'notes', id)) | One doc → DocumentSnapshot. |
| snap.exists() · snap.id · snap.data() | Snapshot inspection. data() is undefined when absent. |
| getDocs(collection(db, 'notes')) | Many docs → QuerySnapshot. |
| qs.docs.map(d => ({ id: d.id, ...d.data() })) | Common “flatten with id” idiom. |
| updateDoc(doc(db,'notes', id), { done: true, updated: serverTimestamp() }) | Partial update. Errors if doc missing. |
| deleteDoc(doc(db,'notes', id)) | Hard delete. |
| serverTimestamp() | Resolved server-side at write. Use for created/updated columns. |
| writeBatch(db).set(...).update(...).delete(...).commit() | Atomic batch — up to 500 writes. |
| runTransaction(db, async (tx) => { ... }) | Optimistic txn with retries on conflict. |
| collection(db, 'users', uid, 'notes') | Subcollection path. |
import { db, auth } from './firebase'
import {
collection, doc, addDoc, setDoc, getDoc, getDocs,
updateDoc, deleteDoc, query, where, orderBy, limit,
onSnapshot, serverTimestamp, writeBatch,
} from 'firebase/firestore'
const uid = auth.currentUser!.uid
// 1. Create — server-assigned ID
const ref = await addDoc(collection(db, 'notes'), {
title: 'Ship firebase cheatsheet',
owner: uid,
done: false,
created: serverTimestamp(), // resolves on the server
})
// 2. Upsert with merge — safe re-create
await setDoc(doc(db, 'notes', ref.id),
{ tags: ['draft'], updated: serverTimestamp() },
{ merge: true })
// 3. Read one
const snap = await getDoc(doc(db, 'notes', ref.id))
if (snap.exists()) console.log(snap.id, snap.data())
// 4. Query + live updates
const q = query(
collection(db, 'notes'),
where('owner', '==', uid),
where('done', '==', false),
orderBy('created', 'desc'),
limit(20),
)
const unsub = onSnapshot(q, (qs) => {
qs.docChanges().forEach((ch) => {
console.log(ch.type, ch.doc.id, ch.doc.data()) // added | modified | removed
})
})
// 5. Atomic batch (up to 500 ops)
const batch = writeBatch(db)
batch.update(doc(db, 'notes', ref.id), { done: true })
batch.delete(doc(db, 'notes', 'stale-id'))
await batch.commit()
// later: unsub() in your effect cleanup
where · orderBy · cursorsFirestore queries
| query(collection(db,'notes'), where('owner','==',uid), orderBy('created','desc'), limit(20)) | Standard composed query. |
| where('priority', 'in', [1,2,3]) | in supports up to 30 values. |
| where('tags', 'array-contains', 'urgent') | Array membership. |
| where('tags', 'array-contains-any', ['a','b']) | Any of several elements. |
| where('created', '>=', threshold) | Range operators: <, <=, >, >=, !=. |
| orderBy('created', 'desc') | First range field must also be the first orderBy. |
| startAfter(lastDoc) · startAt(value) · endBefore(...) | Cursor pagination; pass the last DocumentSnapshot. |
| firestore.indexes.json | Composite indexes live here. Error messages link to the index that’s missing. |
| collectionGroup(db, 'notes') | Query a subcollection name across the whole DB. |
| await getCountFromServer(query(...)) | Server-side count without reading any doc bodies. |
| getAggregateFromServer(q, { total: sum('amount'), avg: average('amount') }) | Sum / average / count in one call. |
onSnapshot · docChangesReal-time listeners
| const unsub = onSnapshot(query(...), snap => ...) | Subscribe. Returns a cleanup function. |
| unsub() | Always call it — orphan listeners burn quota. |
| snap.docChanges().forEach(c => c.type) | added / modified / removed — ideal for diffing UI lists. |
| snap.metadata.hasPendingWrites | true if this is a local optimistic write. |
| snap.metadata.fromCache | true if data is from offline cache, not the server. |
| onSnapshot(q, { includeMetadataChanges: true }, cb) | Fire on metadata-only changes too. |
Per-request authorisationSecurity rules
| service cloud.firestore { match /databases/{db}/documents { ... } } | Top-level wrapper. Required. |
| match /notes/{noteId} { ... } | Per-collection block. |
| allow read: if request.auth != null | Sign-in gate. |
| allow create: if request.auth.uid == request.resource.data.owner | request.resource = incoming doc. |
| allow update: if resource.data.owner == request.auth.uid | resource = existing doc. |
| allow update: if request.resource.data.owner == resource.data.owner | Immutability check — field can’t be changed even by the owner. |
| match /users/{uid}/{document=**} { ... } | Wildcard subtree match. |
| function isOwner() { return request.auth.uid == resource.data.owner; } | Reusable predicate. |
| exists(/databases/$(db)/documents/admins/$(request.auth.uid)) | Cross-doc existence check — admin gate pattern. |
| get(/databases/$(db)/documents/users/$(uid)).data.role == 'admin' | Cross-doc field read. |
| request.time · request.resource.data · resource.data | Built-in vars available in every rule. |
| match /{document=**} { allow read, write: if false; } | Default-deny safety net at the bottom. |
// firestore.rules — owner-only access pattern with a small admin helper.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helpers (reusable predicates)
function isSignedIn() { return request.auth != null; }
function isOwner() { return request.auth.uid == resource.data.owner; }
function becomesOwn() { return request.auth.uid == request.resource.data.owner; }
function isAdmin() {
return exists(/databases/$(database)/documents/admins/$(request.auth.uid));
}
// Per-user private notes
match /notes/{noteId} {
allow read: if isSignedIn() && (isOwner() || isAdmin());
allow create: if isSignedIn() && becomesOwn()
&& request.resource.data.created == request.time;
allow update: if isOwner()
&& request.resource.data.owner == resource.data.owner; // owner immutable
allow delete: if isOwner();
}
// Wildcard subtree — every doc under /users//...
match /users/{uid}/{document=**} {
allow read, write: if isSignedIn() && request.auth.uid == uid;
}
// Default deny
match /{document=**} {
allow read, write: if false;
}
}
}
Email · OAuth · anonymousAuthentication
| getAuth(app) | One Auth instance per app. |
| await createUserWithEmailAndPassword(auth, email, pwd) | Sign-up. |
| await signInWithEmailAndPassword(auth, email, pwd) | Sign-in. |
| await signInWithPopup(auth, new GoogleAuthProvider()) | OAuth popup — desktop-friendly. |
| await signInWithRedirect(auth, provider) · getRedirectResult(auth) | Mobile-safer flow. Call getRedirectResult on the callback page. |
| await signInAnonymously(auth) | Guest user. Can be linked to a real account later. |
| await sendPasswordResetEmail(auth, email) | Email-based reset link. |
| await updateProfile(auth.currentUser, { displayName, photoURL }) | Set display fields. |
| await updateEmail(user, newEmail) · updatePassword(user, newPwd) | Both require a recent sign-in. |
| onAuthStateChanged(auth, user => ...) | Top-level sign-in state subscription. |
| await user.getIdToken(/* forceRefresh */ true) | Get a fresh JWT to send to your backend. |
| await signOut(auth) | Clears the local session. |
import { auth } from './firebase'
import {
createUserWithEmailAndPassword, signInWithEmailAndPassword,
signInWithPopup, signInWithRedirect, getRedirectResult,
signInAnonymously, sendPasswordResetEmail, updateProfile,
onAuthStateChanged, signOut,
GoogleAuthProvider, GithubAuthProvider,
} from 'firebase/auth'
// 1. Email + password
await createUserWithEmailAndPassword(auth, 'ada@example.com', 'correct horse battery')
const cred = await signInWithEmailAndPassword(auth, 'ada@example.com', 'correct horse battery')
// 2. OAuth — popup vs redirect (mobile-safer)
await signInWithPopup(auth, new GoogleAuthProvider())
// or:
await signInWithRedirect(auth, new GithubAuthProvider())
const after = await getRedirectResult(auth) // call on the callback page
// 3. Anonymous (great for "try before sign up")
await signInAnonymously(auth)
// 4. Profile + password resets
await updateProfile(auth.currentUser!, { displayName: 'Ada Lovelace' })
await sendPasswordResetEmail(auth, 'ada@example.com')
// 5. React to sign-in state across the whole app
const unsub = onAuthStateChanged(auth, async (user) => {
if (!user) return navigate('/login')
const token = await user.getIdToken() // JWT for your backend
setHeader('Authorization', `Bearer ${token}`)
})
// later: await signOut(auth); unsub();
Files in Cloud StorageStorage
| import { getStorage, ref, uploadBytes, uploadBytesResumable, getDownloadURL, deleteObject } from 'firebase/storage' | Modular imports. |
| const storage = getStorage(app) | One storage handle per app. |
| const r = ref(storage, 'avatars/' + uid) | Build a reference. Slash-separated path. |
| await uploadBytes(r, file) | One-shot upload. |
| uploadBytesResumable(r, file).on('state_changed', snap => snap.bytesTransferred / snap.totalBytes) | Resumable upload with progress. |
| await getDownloadURL(r) | Token-signed URL — works regardless of bucket visibility. |
| await deleteObject(r) | Hard delete. |
| storage.rules: match /avatars/{uid} { allow read: if true; allow write: if request.auth.uid == uid } | Storage has its own rules language — mirror Firestore patterns. |
Gen 2 · HTTPS · triggers · cronCloud Functions
| import { onRequest, onCall } from 'firebase-functions/v2/https' | HTTPS endpoints + callable. |
| import { onDocumentCreated, onDocumentWritten } from 'firebase-functions/v2/firestore' | Firestore triggers. |
| import { onSchedule } from 'firebase-functions/v2/scheduler' | Cron-style schedules. |
| export const api = onRequest({ region, cors }, (req, res) => ...) | Plain HTTPS handler. cors: true enables wildcard CORS. |
| export const hello = onCall((req) => ({ uid: req.auth?.uid })) | Callable — auth context resolved server-side. |
| export const onNew = onDocumentCreated('notes/{id}', async (event) => ...) | Doc trigger — event.data is the new snapshot. |
| export const nightly = onSchedule('every day 02:00', async () => ...) | Cloud Scheduler under the hood. Cron syntax also accepted. |
| defineSecret('OPENAI_KEY') | Declare a secret. Bind via { secrets: [...] } on the function. |
| throw new HttpsError('permission-denied', 'reason') | Typed error from onCall — surfaces cleanly in the client SDK. |
// Cloud Functions for Firebase — Gen 2 (Cloud Run under the hood).
import { onRequest, onCall, HttpsError } from 'firebase-functions/v2/https'
import { onDocumentCreated } from 'firebase-functions/v2/firestore'
import { onSchedule } from 'firebase-functions/v2/scheduler'
import { defineSecret } from 'firebase-functions/params'
import { initializeApp } from 'firebase-admin/app'
import { getFirestore } from 'firebase-admin/firestore'
initializeApp()
const OPENAI = defineSecret('OPENAI_KEY') // bound at deploy
// 1. HTTPS endpoint
export const api = onRequest(
{ region: 'us-central1', cors: true, secrets: [OPENAI] },
async (req, res) => {
res.json({ ok: true, key_prefix: OPENAI.value().slice(0, 5) })
},
)
// 2. Callable — auth context resolved server-side
export const sayHello = onCall(async (req) => {
if (!req.auth) throw new HttpsError('unauthenticated', 'sign in first')
return { hello: req.auth.uid }
})
// 3. Firestore trigger — fires on every new /notes doc
export const onNewNote = onDocumentCreated(
{ document: 'notes/{noteId}', region: 'us-central1' },
async (event) => {
const before = event.data?.data()
await event.data?.ref.update({ ingestedAt: new Date() })
},
)
// 4. Scheduled — cron syntax or "every day 02:00"
export const nightly = onSchedule('every day 02:00', async () => {
const stale = await getFirestore().collection('notes')
.where('done', '==', true).get()
await Promise.all(stale.docs.map((d) => d.ref.delete()))
})
When to pick whichRealtime DB vs Firestore
| Realtime Database | Single JSON tree. Deep onValue listeners. Pricing: bandwidth + storage. |
| Firestore | Document model. Indexed queries. Scales horizontally. Pricing: per-op + storage + bandwidth. |
| Prefer RTDB when | Very-low-latency presence, ephemeral counters, <100k connected clients per shard. |
| Prefer Firestore when | Anything else, especially anything multi-region or with complex queries. |
| import { getDatabase, ref, onValue, set } from 'firebase/database' | RTDB modular imports if you need it. |
Server-side · bypasses rulesAdmin SDK
| import { initializeApp, cert } from 'firebase-admin/app' | Admin entry point. |
| initializeApp({ credential: cert(serviceAccount) }) | Use a service-account JSON. Never ship to the browser. |
| await getAuth().setCustomUserClaims(uid, { role: 'admin' }) | Embed claims in the JWT — available in rules as request.auth.token.role. |
| await getAuth().verifyIdToken(token) | Validate an incoming JWT in your backend. |
| await getAuth().listUsers(1000, pageToken) | Paged user listing. |
| await getFirestore().collection('notes').doc(id).delete() | Bypasses Firestore rules — for cleanup jobs and admin tooling only. |
| getFirestore().bulkWriter() | High-throughput batched writes with auto-retry. |
Push notificationsCloud Messaging
| getMessaging() · getToken(messaging, { vapidKey }) | Client: subscribe this device. Token = the push address. |
| onMessage(messaging, payload => ...) | Foreground messages — service worker handles background. |
| getMessaging().send({ token, notification: { title, body } }) | Server: send to one device. |
| getMessaging().subscribeToTopic(tokens, 'newsletter') | Topic-based fan-out. |
| getMessaging().send({ topic: 'newsletter', notification: {...} }) | Server-side broadcast. |
Local dev stackEmulator suite
| firebase init emulators | Pick services + ports. |
| firebase emulators:start --import=./seed --export-on-exit=./seed | Seed in, snapshot out — deterministic local dev. |
| connectFirestoreEmulator(db, 'localhost', 8080) | Point the JS SDK at local Firestore. |
| connectAuthEmulator(auth, 'http://localhost:9099') | Local auth — no real users created. |
| connectStorageEmulator(storage, 'localhost', 9199) | Local storage. |
| UI on http://localhost:4000 | Browse data, test rules, replay function calls. |
Full pipeline · ~30 linesEnd-to-end · Auth-gated notes
Signs in, inserts a note with owner = auth.uid, subscribes to that user’s notes
only (security rules enforce the boundary), updates a row, cleans up the listener and signs out.
// End-to-end: auth-gated notes app in ~30 lines.
// RLS-equivalent enforcement lives in firestore.rules (request.auth.uid == owner).
import { auth, db } from './firebase'
import {
signInWithEmailAndPassword, onAuthStateChanged, signOut,
} from 'firebase/auth'
import {
collection, addDoc, query, where, orderBy, onSnapshot,
updateDoc, doc, serverTimestamp,
} from 'firebase/firestore'
async function main() {
// 1. Sign in
await signInWithEmailAndPassword(auth, 'ada@example.com', '****')
onAuthStateChanged(auth, async (user) => {
if (!user) return console.log('signed out')
// 2. Insert — owner field must equal auth.uid (rules enforce this)
const ref = await addDoc(collection(db, 'notes'), {
title: 'Ship firebase cheatsheet',
owner: user.uid,
done: false,
created: serverTimestamp(),
})
// 3. Subscribe to my own notes only
const q = query(
collection(db, 'notes'),
where('owner', '==', user.uid),
orderBy('created', 'desc'),
)
const unsub = onSnapshot(q, (qs) =>
qs.docChanges().forEach((c) => console.log(c.type, c.doc.id)))
// 4. Complete the note
await updateDoc(doc(db, 'notes', ref.id), { done: true })
// 5. Tidy up
unsub()
await signOut(auth)
})
}
main().catch(console.error)
Best practiceGood to know
request.auth.uid == resource.data.owner isn’t paranoid — it’s the minimum.
onSnapshot listeners.
Forgotten listeners survive component unmount, burn read quota, and keep stale data flowing
into your state. Wire unsub() into useEffect’s cleanup.
firebase emulators:start --import=./seed --export-on-exit=./seed gives you
deterministic local data, free reads/writes, and an offline-first dev loop. Tests get the same setup —
no separate mock infrastructure.
Common trapsWatch out for
firestore.indexes.json and deployed. Otherwise reads fail silently after release.