Firestore Backup and Restore: Schedules, PITR and File Exports
Set up Firestore backups, point-in-time recovery and delete protection. Follow gcloud commands and a Node file backup script, then compare recovery workflows in Chiselbase.
By Chiselbase · Explore the Firestore GUI: Firestore
Set up Firestore recovery before you need it
You can prepare for accidental changes with delete protection, point-in-time recovery (PITR), scheduled backups and managed exports. Set up the protection or create the backup before you need it: enabling PITR does not create seven days of history retroactively. Check the database’s earliestVersionTime for the oldest available moment. Restoring a backup or cloning from a point in time creates a new database, leaving the current database in place.
This guide sets up Google’s recovery options with gcloud, shows a readable file extract and explains which parts Chiselbase brings into the desktop workspace.
Keep recovery settings beside your data. Chiselbase Free lets you manage delete protection, PITR and backup schedules. Pro adds restore and clone workflows. For supported changes made in the app, local Undo can be the shortest route back. See the recovery workflow.
Way 1: Google’s tools with gcloud
When a command takes --database, quote (default). The parentheses mean something to your shell. Restore and clone use separate source and destination flags.
Use a current Google Cloud CLI in Bash or Google Cloud Shell. PowerShell does not use \ for line continuation; put each command on one line there. Replace uppercase placeholders and example dates. Clone and PITR export timestamps must be in the past and no earlier than the database’s earliestVersionTime.
Backups and PITR require billing (the Blaze plan). Your caller needs IAM permissions for each operation: roles/datastore.backupSchedulesAdmin for schedules, roles/datastore.backupsViewer for listing backups and roles/datastore.restoreAdmin for restoring. Updating database settings and cloning need their own database permissions; roles/datastore.owner covers them. The backup prerequisites and PITR guide explain the roles.
Stop the database from being deleted
gcloud config set project PROJECT_IDgcloud firestore databases update --database="(default)" --delete-protectionThis blocks deleting the whole database. It does not stop anyone from deleting documents or collections.
Turn on point-in-time recovery
gcloud firestore databases update --database="(default)" --enable-pitrWith PITR, Firestore keeps 7 days of versions, readable to the minute. Without it, you can read the last hour. PITR storage is billed. The window starts filling when you switch it on.
Schedule backups
# Daily, kept for 7 daysgcloud firestore backups schedules create --database="(default)" \ --recurrence=daily --retention=7d
# Weekly, kept for up to 14 weeksgcloud firestore backups schedules create --database="(default)" \ --recurrence=weekly --day-of-week=SUN --retention=14wFirestore allows one daily and one weekly schedule per database. Retention can be up to 14 weeks for either schedule. You cannot choose an exact time of day. The first backup appears after the first run, not when you create the schedule. Backup storage and restore operations are billed separately.
Restore a backup or clone a point in time
Both create a new database. Your live database stays untouched.
# Find the backupgcloud firestore backups list --format="table(name, database, state)"
# Restore it into a new databasegcloud firestore databases restore \ --source-backup=projects/PROJECT_ID/locations/LOCATION/backups/BACKUP_ID \ --destination-database=restored-2026-09-30
# Or clone the database as it was at a minute inside the PITR windowgcloud firestore databases clone \ --source-database="projects/PROJECT_ID/databases/(default)" \ --snapshot-time=2026-09-30T08:00:00Z \ --destination-database=before-the-deleteGetting lost documents back into (default) is a second step: copy them from the restored database, for example with a Chiselbase migration between databases or an SDK script configured for that database. The file script below uses (default) unless you adapt getFirestore(). Restore and clone operations incur charges, and the new database costs storage until you delete it.
Back up to a bucket with the managed export
gcloud firestore export gs://BUCKET/backups/2026-09-30 --database="(default)"
# Import writes into the existing database. Documents with the same ID are overwritten,# documents that are not in the export stay as they are.gcloud firestore import gs://BUCKET/backups/2026-09-30 --database="(default)"This needs the Blaze plan, a bucket, the role roles/datastore.importExportAdmin and a Cloud Storage role on the bucket (Google’s export page names Owner or Storage Admin). The files are binary. The details are in how to export Firestore to JSON, CSV or JSONL.
Way 2: a backup file you can read, with a Node script
Google’s backups live in Google’s format. If you want a copy on your own disk that you can search, the script below walks top-level collections and subcollections beneath existing documents. It writes one tagged JSON line per document. Tags preserve timestamps, references, geopoints, bytes, vectors and special numbers; JavaScript numeric precision and missing parent documents remain limitations.
Use Node.js 22 or later; the example was checked with firebase-admin 14.5.0. Save it as firestore-backup.mjs. The restore defaults to a parsing dry run; add --write only after reviewing the file and target. Keep service account keys outside the repository.
npm init -ynpm install firebase-adminexport GOOGLE_APPLICATION_CREDENTIALS="$HOME/keys/my-project-backup.json"The key needs roles/datastore.viewer for a backup and roles/datastore.user for a restore. On Windows, set the variable with $env:GOOGLE_APPLICATION_CREDENTIALS = "C:\keys\my-project-backup.json".
Show firestore-backup.mjs — complete script
// Usage: node firestore-backup.mjs backup <file.jsonl>// node firestore-backup.mjs restore <file.jsonl> [--write]import { createReadStream } from "node:fs";import { open } from "node:fs/promises";import { createInterface } from "node:readline";import { initializeApp, applicationDefault } from "firebase-admin/app";import { getFirestore, FieldPath, Timestamp, GeoPoint, DocumentReference, FieldValue,} from "firebase-admin/firestore";
const PAGE_SIZE = 300;const FLUSH_EVERY = 2000;const VectorValue = FieldValue.vector([]).constructor;
const [command, file, flag, ...extra] = process.argv.slice(2);if (!["backup", "restore"].includes(command) || !file || extra.length || (flag && (command !== "restore" || flag !== "--write"))) { console.error("Usage: node firestore-backup.mjs <backup|restore> <file.jsonl> [--write]"); process.exit(1);}const WRITE = flag === "--write";
initializeApp({ credential: applicationDefault() });const db = getFirestore();
// Tags preserve special values without confusing them with user maps.function encode(value) { if (typeof value === "number" && !Number.isFinite(value)) return { $t: "number", v: String(value) }; if (value === null || typeof value !== "object") return value; if (value instanceof Timestamp) return { $t: "ts", s: value.seconds, n: value.nanoseconds }; if (value instanceof GeoPoint) return { $t: "geo", lat: value.latitude, lng: value.longitude }; if (value instanceof DocumentReference) return { $t: "ref", path: value.path }; if (value instanceof Uint8Array) return { $t: "bytes", b64: Buffer.from(value).toString("base64") }; if (value instanceof VectorValue) return { $t: "vector", v: value.toArray() }; if (Array.isArray(value)) return value.map(encode); return { $t: "map", v: Object.fromEntries(Object.entries(value).map(([k, v]) => [k, encode(v)])) };}
function decode(value) { if (value === null || typeof value !== "object") return value; if (Array.isArray(value)) return value.map(decode); switch (value.$t) { case "number": return Number(value.v); case "ts": return new Timestamp(value.s, value.n); case "geo": return new GeoPoint(value.lat, value.lng); case "ref": return db.doc(value.path); case "bytes": return Buffer.from(value.b64, "base64"); case "vector": return FieldValue.vector(value.v); case "map": return Object.fromEntries(Object.entries(value.v).map(([k, v]) => [k, decode(v)])); default: throw new Error(`Unknown value in backup: ${JSON.stringify(value)}`); }}
async function backupCollection(ref, write, stats) { let last = null; for (;;) { let query = ref.orderBy(FieldPath.documentId()).limit(PAGE_SIZE); if (last) query = query.startAfter(last); const page = await query.get(); for (const doc of page.docs) { await write(`${JSON.stringify({ path: doc.ref.path, data: encode(doc.data()) })}\n`); stats.documents += 1; // One extra request per document to find its subcollections. for (const sub of await doc.ref.listCollections()) await backupCollection(sub, write, stats); } if (page.size < PAGE_SIZE) return; last = page.docs[page.docs.length - 1]; }}
if (command === "backup") { const out = await open(file, "wx"); // refuse to overwrite an existing backup const write = (text) => out.writeFile(text, "utf8"); const stats = { documents: 0 }; try { for (const collection of await db.listCollections()) { console.log(`Backing up ${collection.path} …`); await backupCollection(collection, write, stats); } } finally { await out.close(); } console.log(`${stats.documents} documents written to ${file}.`);} else { const writer = WRITE ? db.bulkWriter() : null; let failed = 0; // A custom handler replaces BulkWriter's default, which retries ABORTED and // UNAVAILABLE up to 10 times. Keep that, and fail every other error at once. const RETRYABLE = new Set([10, 14]); // gRPC status codes ABORTED, UNAVAILABLE writer?.onWriteError((error) => { if (RETRYABLE.has(error.code) && error.failedAttempts < 10) return true; failed += 1; console.error(`Failed: ${error.documentRef.path}: ${error.message}`); return false; }); const input = createReadStream(file, { encoding: "utf8" }); const lines = createInterface({ input, crlfDelay: Infinity }); let queued = 0; try { for await (const line of lines) { if (!line.trim()) continue; const { path, data } = JSON.parse(line); if (typeof path !== "string" || data?.$t !== "map") throw new Error("Expected a document path and tagged map"); const ref = db.doc(path); const fields = decode(data); // set() replaces the document. Failures are counted in onWriteError above. writer?.set(ref, fields).catch(() => {}); queued += 1; if (writer && queued % FLUSH_EVERY === 0) await writer.flush(); } } finally { lines.close(); input.destroy(); if (writer) await writer.close(); } console.log(WRITE ? `${queued - failed} of ${queued} documents restored, ${failed} failed.` : `Dry run: ${queued} documents parsed. Nothing written. Add --write to restore.`); if (failed) process.exitCode = 1;}node firestore-backup.mjs backup backup-2026-09-30.jsonlnode firestore-backup.mjs restore backup-2026-09-30.jsonlnode firestore-backup.mjs restore backup-2026-09-30.jsonl --writeTry a restore against a running local emulator first. In Bash:
unset GOOGLE_APPLICATION_CREDENTIALSexport FIRESTORE_EMULATOR_HOST=127.0.0.1:8080export GCLOUD_PROJECT=demo-restorenode firestore-backup.mjs restore backup-2026-09-30.jsonlnode firestore-backup.mjs restore backup-2026-09-30.jsonl --writeIn PowerShell:
Remove-Item Env:GOOGLE_APPLICATION_CREDENTIALS -ErrorAction SilentlyContinue$env:FIRESTORE_EMULATOR_HOST = "127.0.0.1:8080"$env:GCLOUD_PROJECT = "demo-restore"node firestore-backup.mjs restore backup-2026-09-30.jsonlnode firestore-backup.mjs restore backup-2026-09-30.jsonl --writeUnset FIRESTORE_EMULATOR_HOST when you are done. While it is set, every Firestore instance in the process connects to the emulator.
Costs and limits of this script:
- A backup bills document reads, empty-query minimums and collection-list requests. Each request listing collection IDs is billed as a document read, including extra pages.
- The restore costs one write per document and overwrites documents with the same path.
- Backup output must not already exist. On failure, remove the partial file or choose a new filename and retry. A restore is not atomic; earlier writes remain if a later line fails. Failed server writes set a non-zero exit code.
- A restore dry run validates the file’s paths and tagged values locally; it does not inspect the target or verify server permissions.
- Integers and doubles both arrive as JavaScript numbers. A double like
3.0comes back as the integer3. Integers above 2⁵³ lose precision. - Queries only return documents that exist. A subcollection under a document that doesn’t exist (a “ghost” parent) is never visited, so its documents are missing from the backup.
- This is not a consistent snapshot while writes continue, and it does not back up indexes, Security Rules, TTL policies or other Firebase services. Use a scheduled backup or a PITR export for database recovery.
- Reference tags store document paths. On restore they resolve in the destination database, including references that originally pointed into a different project or database. Adapt the tag to store the full resource name if you need those external references preserved.
To pull single documents out of a restored database into the live one, point a second Firestore instance at it with getFirestore(app, "restored-2026-09-30").
You can also read a document as it was, without restoring anything. Inside the last hour this works without PITR. Further back it needs PITR and a whole minute:
import { initializeApp, applicationDefault } from "firebase-admin/app";import { getFirestore, Timestamp } from "firebase-admin/firestore";
initializeApp({ credential: applicationDefault() });const db = getFirestore();
const readTime = Timestamp.fromMillis(Date.now() - 30 * 60 * 1000); // 30 minutes agoconst before = await db.runTransaction((tx) => tx.get(db.doc("orders/ord_10384")), { readOnly: true, readTime,});console.log(before.exists ? before.data() : "The document did not exist at that time.");Way 3: with Chiselbase
Chiselbase puts Google’s recovery settings into one view beside your collections. Recovery-setting changes and initiated restore or clone operations appear in its audit log. Google’s billing and IAM requirements still apply, including when the Chiselbase feature is Free.
Set up recovery (Free). Open Firestore → Backups. There you can:
- turn on Delete protection,
- turn on Point-in-time recovery (PITR),
- create Backup schedules with Create schedule.
These three are part of Chiselbase Free, on your one cloud project.
· backups · (default)
Delete protection and PITR on, two backup schedules, six backups. Recovery settings, scheduled backups and restore actions share one view.
Restore (Pro). From the same view, Restore from backup and Clone to a point in time create a new database, as Google’s API always does. Time travel reads a document as it was at a point in the PITR window, without writing anything. Exporting a collection to FS-JSON with its subcollections, and importing that file again, is also Pro. Restoring is part of Chiselbase Pro; the 14-day trial starts inside the app.
Restore supported Chiselbase changes with Undo (Free). Chiselbase attempts to save a local snapshot for Firestore document edits and deletes, completed bulk runs and whole-collection deletes. Undo requires a successfully saved snapshot; a collection delete still runs if its snapshot fails. Moving or copying documents and changes made outside Chiselbase have no undo. Metadata-only and none policies disable undo. See audit and undo coverage for the full limits.
Two things also help before anything goes wrong. A new production connection starts write-protected. And production asks you to type the project ID before a destructive action. Details are on the safety page.
Which way to pick
| Recovery option | What it covers | Recovery and cost |
|---|---|---|
| Delete protection | Accidental deletion of the entire database | No recovery operation or extra protection charge. Does not protect documents. |
| PITR | A retained moment within up to 7 days | Historical reads, clone to a new database or PITR export. Version storage, reads and clone charges apply. |
| Scheduled backup | The state at a daily or weekly backup, retained up to 14 weeks | Restore to a new database. Backup storage and restore are billed. |
| Managed export | Documents in the exported database or selected groups | Import into an existing database. Document reads/writes and bucket storage are billed. |
| Tagged script extract | A readable copy of documents reachable by this script | Restore into an existing database. Read/write charges; not a complete or consistent database backup. |
| Chiselbase | Google’s recovery options in one view, plus local Undo for supported app changes | Settings and Undo are Free; restore, clone and file export/import are Pro. Google’s charges still apply. |
Start with delete protection, then choose PITR and a backup schedule according to how much history you need and your recovery budget. A readable extract is useful for inspection, with the script’s limits above. Chiselbase brings Google’s controls into the same workspace and adds local Undo for supported changes made in the app.
Limits and questions
Can I restore a single collection from a scheduled backup?
Not directly. A restore always creates a whole new database. Restore it, then copy the collection you need into (default) with a script or with a migration between the two databases.
Does PITR cost extra?
Yes. Firestore bills the storage for the version history. The size depends on how much your data changes, so check the pricing page for your region.
Is a managed export a consistent snapshot?
Not by default. Writes during the export may or may not be included. With PITR on, gcloud firestore export --snapshot-time exports one consistent moment.
I deleted a collection a minute ago. What now?
If PITR is on, or the delete is less than an hour old, clone the database to the minute before and copy the collection back. If you deleted it in Chiselbase, try Undo from the Audit log first.
To put those controls next to the data you work on, download Chiselbase. Recovery settings and supported local Undo are Free; start the 14-day Pro trial in the app when you want to restore, clone or export a file. See Free and Pro for the plan limits.
Sources and verification
Checked on 4 October 2026 against Google’s backup documentation, PITR guide, clone CLI reference and pricing. The file script was exercised against a local emulator. Cloud commands and historical-read options were checked against the official references; a local emulator does not verify cloud recovery behavior. Chiselbase steps were checked against the current implementation.
Next: how to copy Firestore data between projects. What else Chiselbase does with Firestore is on the Firestore page.