How to Edit Firestore Documents Without Breaking Your Data

Updated

Editing a Firestore document is easy. Editing it without changing something you didn't mean to is harder. The usual culprits are type conversions, write semantics, and concurrent edits. This guide covers each, with what to do in code and what to expect from a Firestore editor.

Firestore types that JSON can't represent

Firestore typeWhat goes wrong in plain JSON
TimestampBecomes a string or {"_seconds": ..., "_nanoseconds": ...}; writing it back stores a string or a map, not a timestamp
ReferenceBecomes a path string; writing it back stores a string, breaking joins
64-bit integerJavaScript numbers lose precision above 253, so IDs and counters get rounded
Double with no fraction (e.g. 1.0)Round-trips as the integer 1, changing its type
GeoPoint, BytesBecome maps or base64 strings

Your app may still work after these changes, until a query compares by type, a client SDK expects a Timestamp, or a rounded ID points at the wrong record. Edit with a tool that keeps types explicit. Firewatch shows them as tagged values, such as {"$timestamp": "2024-05-01T12:00:00Z"} and {"$ref": "users/alice"}.

set, update, and merge do different things

  • set(data) replaces the whole document. Fields you leave out are deleted.
  • set(data, { merge: true }) changes only the fields you pass. Fields you leave out are kept, so removing a key from your object does not delete it.
  • update(data) changes only the given fields, and fails if the document doesn't exist.
  • To delete a single field, use FieldValue.delete() in an update.

A common bug in homemade editors is using merge for saves: you delete a field in the editor, save, and it's still there. A full-document editor should replace the document, so what you see is exactly what gets stored.

Don't overwrite someone else's changes

If you open a document, a teammate or your app changes it, and then you save, a plain set silently undoes their change. In code, use a transaction or a precondition on the document's update time:

const snap = await ref.get();
// ...user edits newData...

// Unsafe: await ref.set(newData)  (last write wins)
// Safe: only write if nothing changed since we read it
await db.runTransaction(async (tx) => {
  const current = await tx.get(ref);
  if (!current.updateTime.isEqual(snap.updateTime)) {
    throw new Error("Document changed since you loaded it");
  }
  tx.set(ref, newData);
});

Firewatch saves with an update-time precondition, so a stale save is refused and you're asked to reload.

Deleting safely

  • Deleting a document does not delete its subcollections. Delete those separately if you mean to.
  • There's no recycle bin. Enable point-in-time recovery or scheduled backups on production databases so you can recover from mistakes.
  • For day-to-day work, an undo helps: Firewatch keeps the deleted documents in memory briefly and can restore them, unless something new has been written at the same path since.

A safe editing checklist

  1. Browse production in read-only mode until you need to write.
  2. Check field types before and after editing.
  3. Save full documents with a precondition; never blind-merge.
  4. Export the collection first before bulk changes (see exporting to JSON).
  5. Make sure your IAM role is no broader than you need (see Firestore IAM roles).
    How to Edit Firestore Documents Without Breaking Your Data | Firewatch.io