Practical Firestore guide

Deleted a Firestore document, but its children are still there?

Deleting a Firestore parent does not cascade to its children. Reproduce the behavior locally, inspect exact paths and choose an explicit cleanup boundary.

Examples use synthetic data. AI assistance was used in preparation; verification scope is stated below.

Deleting a parent document with the SDK does not mean deleting the tree below it. A missing parent and readable child documents can both be real at the same time.

Disclosure: This AI-assisted technical article was prepared for Cengal, a Firestore developer tool. All examples and original code-drawn illustrations use synthetic data. No Cengal account, subscription or paid product is needed to follow it.

A reported symptom, not a diagnosis of someone else’s system

On June 24, 2021, a developer reported on Stack Overflow that deleted team documents left player documents behind, while querying teams did not reveal those missing parents.

That report includes cleanup-function code and later errors. We have not reproduced that entire application or established its complete cause. The narrower behavior is documented by Firebase’s deletion guide: a document deletion does not cascade into its subcollections. An absent parent is not returned as a document in a normal parent-collection query.

This is worth checking before treating a remaining child as a failed delete or a stale UI.

Synthetic path diagram: teams/demo is absent after deleting the parent, while teams/demo/players/ada remains readable.
Original AI-assisted code-drawn diagram. Two synthetic document paths; no app screenshot.

Reproduce it with two disposable documents

Use the Node.js server SDK @google-cloud/firestore in a scratch project. Start a local Firestore emulator on 127.0.0.1:8080 with project ID demo-delete-example, following Firebase’s emulator connection guide. Set FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 before running this CommonJS script.

const { Firestore } = require('@google-cloud/firestore');
if (process.env.FIRESTORE_EMULATOR_HOST !== '127.0.0.1:8080') {
  throw new Error('This example requires the local Firestore emulator');
}
const db = new Firestore({ projectId: 'demo-delete-example' });

async function run() {
  const team = db.doc('teams/demo');
  const player = team.collection('players').doc('ada');
  await team.set({ name: 'Demo team' });
  await player.set({ name: 'Ada' });

  await team.delete(); // deletes this document, not its descendants
  console.log((await team.get()).exists); // false
  console.log((await player.get()).exists); // true
  console.log((await db.collection('teams').get()).size); // 0
  console.log((await team.collection('players').get()).size); // 1

  await player.delete(); // clean up this one known synthetic child
  console.log((await player.get()).exists); // false
}
run().catch(error => {
  console.error(error);
  process.exitCode = 1;
}).finally(() => db.terminate());

The guard deliberately refuses a different or missing emulator endpoint. The demo- project ID is another boundary. Do not adapt this example by pointing it at a real team or account. If you choose another loopback port, change both the environment value and the guard.

Two paths, two outcomes

The parent teams/demo stores one document’s fields. The child teams/demo/players/ada is a separate document at another path. Removing the first does not remove the second. A reference to the child still identifies its path even when the parent document no longer exists.

The empty teams query therefore answers a narrow question: which existing team documents match? It is not an inventory of every descendant stored beneath team paths. Check a known child reference separately when investigating this symptom.

Original cartoon: a surviving child document smiles and says: I still live here. The takeaway is that one document delete is not a tree delete.
Original AI-assisted code-drawn humor; no third-party clips. Normal motion autoplays; reduced motion, no JavaScript and load failure use this still.

Choose the intended deletion boundary

If you meant to delete only the parent record, surviving children may be expected. Decide whether the application should keep or expose them; do not infer the intended retention policy from the document hierarchy alone.

If you meant to remove a whole subtree, use a separately designed cleanup operation. Firebase’s recursive-deletion solution describes a trusted-server approach and warns about authorization, cost and incomplete work. Review that guidance rather than turning the tiny example above into a general recursive-deletion utility.

For a real cleanup, first inventory the exact project, database and paths. Restrict who can request it, coordinate other writers, choose recovery procedures, and make progress and failures visible. Reading and deleting a changing dataset is not automatically atomic: another writer can create children while cleanup runs. A successful parent delete alone proves nothing about completion of a larger cleanup.

What the local test actually proved

On October 22, 2026, nine assertions passed using Firestore emulator 1.22.0 and Node server SDK 7.11.6 on loopback with a demo- project. They checked both documents before deletion; the absent parent and retained child afterward; an empty parent query; the child query and collection-group result; explicit child cleanup; and a second parent delete leaving it absent.

This is a synthetic test of document/path semantics. It does not test production IAM, billing, client Security Rules, a recursive cleanup service, concurrent cleanup, or Cengal’s UI. Server SDK access does not validate mobile/web Security Rules. Different deletion tools can deliberately implement recursion, so check the exact operation you used rather than generalizing from its button label.

One question for your data model

When a parent record is removed, which child records should survive—and where is that retention decision enforced?