Published  · Updated

Export Firestore to JSON or CSV: Scripts and Desktop Tools

Export a Firestore collection to JSON, CSV or JSONL with a Node script or Chiselbase. Compare readable file exports with Google’s managed export, including costs and type handling.

By Chiselbase  · Explore the Firestore GUI: Firestore

Choose the right Firestore export for your task

You want one collection as a file you can open, diff or hand to a colleague. The Google Cloud console and gcloud start a managed export to a Cloud Storage bucket. Neither writes JSON or CSV. The managed export uses a binary format made for re-import, not for reading.

This guide compares managed exports, a runnable file-export script and the desktop workflow.

Already browsing the data? In Chiselbase, export the collection or query result you are looking at, with optional subcollections. Choose FS-JSON when you want to import it back with its Firestore types intact. See the desktop workflow.

Way 1: the managed export with gcloud

This is the official path. It runs on Google’s side and is built for whole databases. It does not produce JSON. Use it for full backups or a BigQuery load, not for a file you want to read.

You need:

  • the project on the Blaze plan, because managed export requires billing,
  • the IAM role roles/datastore.importExportAdmin on the project,
  • a Cloud Storage role on the bucket as well: Google’s export page names Owner or Storage Admin,
  • a Cloud Storage bucket near the database location. It must not be a Requester Pays bucket or a Rapid bucket. Multi-region Firestore location IDs are not necessarily valid bucket location IDs; use Google’s location mapping.

The export job uses a Firestore service agent as well as your own account. A bucket in another project needs explicit access for that agent. Follow the managed export permissions before running the commands. Run Bash examples in Bash or Google Cloud Shell; export and \ line continuations do not work in PowerShell. Replace uppercase placeholders and sample timestamps with your own values.

Terminal window
gcloud config set project PROJECT_ID
# Where does the database live? Create the bucket in a matching location.
# Use this location to choose a supported Cloud Storage bucket location.
gcloud firestore databases describe --database="(default)" --format="value(locationId)"
gcloud storage buckets create gs://BUCKET --location=LOCATION
# Export the whole database …
gcloud firestore export gs://BUCKET --database="(default)"
# … or only some collection groups (required for a BigQuery load)
gcloud firestore export gs://BUCKET --database="(default)" --collection-ids=orders,customers
# Download the result. gcloud prints the folder name (outputUriPrefix).
gcloud storage cp -r gs://BUCKET/2026-09-30T08:15:00_12345 ./firestore-export

What you get is a folder of export files. Typically, though not guaranteed, that is an .overall_export_metadata file and data files named output-0, output-1 and so on, some under all_namespaces/. They use the LevelDB log format. A text editor shows you bytes. gcloud firestore import and BigQuery can read them.

Costs and limits:

  • The export reads every document it includes. You pay for those reads and for the storage in the bucket. These reads do not show up in the console’s usage view. Check the Firestore pricing page before exporting a large database.
  • An export is not an exact snapshot. Writes that happen while it runs may or may not be in the result. With point-in-time recovery enabled, --snapshot-time exports one consistent moment. The time has to be a whole minute, in the past and inside the PITR window (at most 7 days back), written in RFC 3339, like 2026-09-30T08:00:00Z.
  • It runs as a long-running operation. gcloud firestore operations list shows its state.

Way 2: a Node script with firebase-admin

A script gives you real JSON, JSONL or CSV and full control over how each Firestore type is written. It needs Node.js 22 or later and a service account key for cloud access.

Terminal window
mkdir firestore-export && cd firestore-export
npm init -y
npm install firebase-admin
# A key for a service account with at least roles/datastore.viewer
export GOOGLE_APPLICATION_CREDENTIALS="$HOME/keys/my-project-reader.json"

On Windows, in PowerShell:

Terminal window
$env:GOOGLE_APPLICATION_CREDENTIALS = "C:\keys\my-project-reader.json"

Use Node.js 22 or later. The examples were checked with firebase-admin 14.5.0. Keep the key outside your repository. When using the emulator, set GCLOUD_PROJECT to a demo project ID and unset GOOGLE_APPLICATION_CREDENTIALS.

Save this as export-firestore.mjs:

Show export-firestore.mjs — complete script
export-firestore.mjs
// Usage: node export-firestore.mjs <collectionPath> [jsonl|json|csv] [outFile]
import { open } from "node:fs/promises";
import { initializeApp, applicationDefault } from "firebase-admin/app";
import {
getFirestore,
FieldPath,
Timestamp,
GeoPoint,
DocumentReference,
FieldValue,
} from "firebase-admin/firestore";
const PAGE_SIZE = 500;
const VectorValue = FieldValue.vector([]).constructor;
const [collectionPath, format = "jsonl", outArg] = process.argv.slice(2);
if (!collectionPath || !["jsonl", "json", "csv"].includes(format)) {
console.error("Usage: node export-firestore.mjs <collectionPath> [jsonl|json|csv] [outFile]");
process.exit(1);
}
const outFile = outArg ?? `${collectionPath.replaceAll("/", "_")}.${format}`;
// Reads GOOGLE_APPLICATION_CREDENTIALS; the project ID comes from the key file.
initializeApp({ credential: applicationDefault() });
const db = getFirestore();
// JSON has no timestamp, reference, geopoint or bytes type. Decide explicitly.
function toPlain(value) {
if (typeof value === "number" && !Number.isFinite(value)) throw new Error("JSON cannot represent NaN or Infinity; use a tagged format instead");
if (value === null || typeof value !== "object") return value;
if (value instanceof Timestamp) return value.toDate().toISOString(); // keeps milliseconds only
if (value instanceof GeoPoint) return { latitude: value.latitude, longitude: value.longitude };
if (value instanceof DocumentReference) return value.path;
if (value instanceof Uint8Array) return Buffer.from(value).toString("base64");
if (value instanceof VectorValue) return value.toArray();
if (Array.isArray(value)) return value.map(toPlain);
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, toPlain(v)]));
}
// Page through the collection by document ID: one query per PAGE_SIZE documents.
async function* readAll(path) {
let last = null;
for (;;) {
let query = db.collection(path).orderBy(FieldPath.documentId()).limit(PAGE_SIZE);
if (last) query = query.startAfter(last);
const page = await query.get();
for (const doc of page.docs) yield doc;
if (page.size < PAGE_SIZE) return;
last = page.docs[page.docs.length - 1];
}
}
// CSV: nested maps become "a.b" columns, arrays stay JSON inside one cell.
function flatten(obj, prefix = "", row = {}) {
for (const [key, value] of Object.entries(obj)) {
const column = prefix ? `${prefix}.${key}` : key;
if (value && typeof value === "object" && !Array.isArray(value)) flatten(value, column, row);
else row[column] = value;
}
return row;
}
function csvCell(value) {
if (value === undefined || value === null) return "";
const text = typeof value === "string" ? value : JSON.stringify(value);
return /[",\r\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
}
// Exclusive creation prevents an accidental overwrite; file errors reject the awaited write.
const out = await open(outFile, "wx");
const write = (text) => out.writeFile(text, "utf8");
let count = 0;
const csvRows = [];
const columns = new Set(["id"]);
try {
if (format === "json") await write("{\n");
for await (const doc of readAll(collectionPath)) {
const data = toPlain(doc.data());
if (format === "jsonl") {
await write(`${JSON.stringify({ id: doc.id, data })}\n`);
} else if (format === "json") {
await write(`${count ? ",\n" : ""} ${JSON.stringify(doc.id)}: ${JSON.stringify(data)}`);
} else {
const row = { ...flatten(data), id: doc.id };
for (const column of Object.keys(row)) columns.add(column);
csvRows.push(row); // CSV needs every column before the header, so rows wait in memory
}
count += 1;
}
if (format === "json") await write("\n}\n");
if (format === "csv") {
const header = [...columns];
await write("\uFEFF"); // byte order mark, so Excel reads the file as UTF-8
await write(`${header.map(csvCell).join(",")}\r\n`);
for (const row of csvRows) await write(`${header.map((c) => csvCell(row[c])).join(",")}\r\n`);
}
} finally {
await out.close();
}
console.log(`${count} documents from ${collectionPath} written to ${outFile}.`);

Run it:

Terminal window
node export-firestore.mjs orders jsonl
node export-firestore.mjs orders csv orders.csv
node export-firestore.mjs customers/c_1042/orders json

What the script decides for you, and what you may want to change:

  • Timestamps become ISO 8601 strings in UTC. Firestore stores nanoseconds, JavaScript dates keep milliseconds. If you need the exact value, write { seconds, nanoseconds } instead.
  • References become their path, geopoints an object, bytes a base64 string. A later import has to know that.
  • Large integers above 2⁵³ lose precision, because the Admin SDK reads them as JavaScript numbers.
  • Reads: one read per returned document, plus a minimum read charge for an empty query. JSON and JSONL keep memory at one page. This script has no resume checkpoint: after a failure, remove the partial output or choose a new filename and run again. The output file must not already exist.
  • Special numbers and vectors: NaN and infinities stop this plain JSON export rather than silently becoming null. Vectors become arrays. Use the tagged backup script or FS-JSON to preserve these types.
  • Subcollections are not included. Recursive traversal needs additional queries; see the backup script for an example.
  • CSV keeps all rows in memory until the header is known. For very large collections, export JSONL and convert later.
  • CSV is lossy: a field called id is replaced by the document ID; empty maps, missing values and null do not round-trip. Literal dots in field names can collide with flattened paths. When opening untrusted values in a spreadsheet, import cells as text to avoid formula evaluation.
  • Consistency: pages can see different moments if the collection changes during the run. This is a data extract, not a consistent database snapshot.

Way 3: with Chiselbase

Chiselbase exports a collection, a selection of documents or a query result to a file on your disk. You pick the format in the dialog.

  1. Open the connection and go to Firestore → Data.
  2. Open the collection and tick a row. The selection bar offers Select all for the whole collection, including documents not loaded yet.
  3. Click Export. The Export collection dialog asks for a Format: FS-JSON, JSON, JSONL or CSV.
  4. Tick incl. subcollections if you want them. CSV is flat, so the option is off for CSV.
  5. Click Export and choose where to save the file.

For a filtered result, build the query in Firestore → Query builder and use Export results.

· query builder · orders

Chiselbase query builder with the condition status == shipped on the orders collection and a Collection group checkbox. Below, seven matching orders in a table under the buttons Bulk edit and Export results.

orders where status == shipped, 7 results. Bulk edit and export sit right above them.

Format What it is for Subcollections Chiselbase can import it back
FS-JSON (default) Keeps every Firestore type yes yes
JSON Reading, other tools yes no
JSONL One document per line, large collections yes yes
CSV Spreadsheets, nested fields as a.b columns no yes

Chiselbase JSONL uses records shaped as {"__path__":"orders/ord_10384","fields":{…}}, with Firestore wire-format fields. The script above writes plain { id, data } records instead; those JSONL files cannot be imported directly into Chiselbase. FS-JSON avoids type conversion for a Chiselbase round trip.

Import and export is part of Chiselbase Pro; the 14-day trial starts inside the app. No card is needed to start it.

Which way to pick

Approach Best fit Keep in mind
Managed export Database recovery or BigQuery Billing, bucket and IAM required; binary output. A filtered export includes only the named collection groups.
Node script CI jobs or custom JSON/CSV You maintain the encoding. This example exports one collection without subcollections.
Chiselbase Inspecting data and saving a collection, selection or query result Pick a format in the same workspace. FS-JSON keeps types; subcollections are optional except in CSV. Pro or trial required.

If you only need a backup, the managed export is the right tool. If you export the same collection every week in CI, write the script. If you export while you work in the data, Chiselbase saves you the script.

Limits and questions

Does exporting cost money?

Exports consume Firestore reads. Whether you receive an extra bill depends on your free quota and billing plan. Managed exports bill each exported document and add bucket costs; SDK exports may also charge for empty queries, collection discovery or index entries in filtered queries. See Firestore pricing.

Why are my timestamps different in the JSON file?

JSON has no timestamp type. This script converts timestamps to ISO strings through JavaScript Date, losing sub-millisecond precision. ISO strings can represent more precision when an exporter preserves it. FS-JSON and the { seconds, nanoseconds } form keep the timestamp value without a Date conversion.

Can I turn a managed export into JSON?

Not directly. The files are meant for gcloud firestore import and BigQuery. One way is to load a --collection-ids export into BigQuery and export the table from there as newline-delimited JSON.

Can Excel open the CSV?

Yes. The script writes a UTF-8 byte order mark. If your Excel locale expects semicolons, use Data → From Text/CSV and choose comma as the delimiter. Nested maps arrive as a.b columns, arrays as JSON text in one cell.

For the next export you make while inspecting data, download Chiselbase and start the 14-day Pro trial in the app. You can choose the format and scope without maintaining an export script.

Sources and verification

Checked on 4 October 2026 against Google’s managed export documentation, pricing and the current Chiselbase implementation. The Node example was exercised against a local Firestore emulator; cloud commands were checked against the CLI documentation.

Next: how to back up Firestore and restore it and how to import CSV or JSON into Firestore. What else Chiselbase does with Firestore data is on the Firestore page.