Sooner or later you need a Firestore collection as a file. A backup before a migration, a CSV for someone who lives in Google Sheets, or something you can grep. You’d expect an Export button. There isn’t one.
What the official tools give you
The Firebase console lets you view, add, edit and delete documents. It has no export to JSON or CSV.
Managed export (gcloud firestore export, or Import/Export in the Google Cloud console) is the official route, and it’s built for backups, not for reading:
gcloud firestore export gs://my-bucket/backups --collection-ids=users
- It needs billing enabled (the Blaze plan) and a Cloud Storage bucket.
- The output is in LevelDB log format with protobuf metadata. You can’t open it in an editor, and you can’t import it anywhere except back into Firestore (or BigQuery, if you exported specific collections).
--collection-idsmatches collection groups. Subcollections aren’t included unless you list them too.- It isn’t a consistent snapshot unless you pass
--snapshot-time. Documents that change during the export can be exported mid-change. - Each exported document is billed as one read, and those reads don’t show up on the usage dashboard.
Good for disaster recovery. Useless if you want users.json.
A script that writes JSON
If you have a service account or gcloud auth application-default login, this is about as short as it gets:
import { initializeApp, applicationDefault } from 'firebase-admin/app';
import { getFirestore, FieldPath } from 'firebase-admin/firestore';
import { createWriteStream } from 'node:fs';
initializeApp({ credential: applicationDefault(), projectId: 'my-project' });
const db = getFirestore();
const out = createWriteStream('users.ndjson');
let last = null;
for (;;) {
let q = db.collection('users').orderBy(FieldPath.documentId()).limit(1000);
if (last) q = q.startAfter(last);
const page = await q.get();
if (page.empty) break;
for (const doc of page.docs) {
out.write(JSON.stringify({ id: doc.id, ...plain(doc.data()) }) + '\n');
}
last = page.docs.at(-1);
}
out.end();
Two things in there are on purpose.
It pages. collection('users').get() works fine on 200 documents and runs out of memory on 2 million. Ordering by document ID and using startAfter walks the collection in chunks of 1,000.
It writes NDJSON, one document per line. You can stream it, grep it, and turn it into a JSON array later if you need one.
And plain(), because JSON.stringify on Firestore values goes badly. Timestamps come out as {"_seconds": …}, and a DocumentReference drags the whole client along with it:
import { Timestamp, DocumentReference, GeoPoint } from 'firebase-admin/firestore';
function plain(v) {
if (v instanceof Timestamp) return v.toDate().toISOString();
if (v instanceof DocumentReference) return v.path;
if (v instanceof GeoPoint) return { lat: v.latitude, lng: v.longitude };
if (Buffer.isBuffer(v)) return v.toString('base64');
if (Array.isArray(v)) return v.map(plain);
if (v && typeof v === 'object') {
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, plain(x)]));
}
return v;
}
The catch: this is lossy. A timestamp becomes a string, a reference becomes a path. If you import the file back, they stay strings unless you convert them again. For a real backup, keep the types (managed export does), or write your own typed format.
Turning it into CSV
Firestore documents don’t have a fixed shape, and CSV needs one. What works:
- One column per top-level field, using the union of keys across all documents, not just the first one.
- Nested maps and arrays go in as JSON text in a single cell.
- Timestamps as ISO 8601, references as paths.
Collect the keys in one pass over the NDJSON, then write rows in a second pass. Quote every cell and double any quotes inside it, and Sheets and Excel will open it fine.
Exporting a query, not the whole collection
Add the where() before orderBy(FieldPath.documentId()). With equality filters, that works without a custom index:
db.collection('users').where('status', '==', 'trial').orderBy(FieldPath.documentId())
Range filters (>, <) need the first orderBy on the same field, so order by that field and then by document ID.
From a phone
I got tired of keeping this script around, so Firedeck does it. Open a collection, filter it if you want, tap Export, and pick a format:
- Firedeck backup: keeps every type exactly (references, geopoints, bytes, full-precision timestamps), so it imports back without loss.
- JSON or NDJSON: readable, and they import back too.
- CSV: one column per top-level field, nested values as JSON text.
It pages the same way the script does, streams to a file, and saves to Files or iCloud Drive. Each document is still one read; that’s how Firestore bills. Bulk export is part of Firedeck Pro, and copying a single document as JSON is free. More in the export docs.