#proper deletion

5 messages · Page 1 of 1 (latest)

turbid vault
#

is there a good way to delete documents that are related to each other, through ids? i see that there is no cascade delete, but how would i do that without manually deleting everything after one another. thats not scalable

vital canopyBOT
#

Hi @turbid vault. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Convex doesn’t have built‑in cascading deletes, but there are a few supported patterns to make this manageable and scalable.

1. Simple cascade in a single mutation (small fan‑out)

For relationships where each document only has a small number of dependents, you can delete related docs in the same mutation using queries + ctx.db.delete. This is transactional but bounded by mutation limits. [Generic writer]

Example pattern (user → messages):

export const deleteUser = mutation({
  args: { userId: v.id("users") },
  handler: async (ctx, { userId }) => {
    // Delete related messages
    const messages = await ctx.db
      .query("messages")
      .withIndex("owner", q => q.eq("owner", userId))
      .collect();

    for (const message of messages) {
      await ctx.db.delete(message._id);
    }

    // Finally delete the user
    await ctx.db.delete("users", userId);
  },
});

This is essentially what the triggers article shows for cascade deletes. [Cascade deletes]

2. Triggers for automatic cascades (still bounded)

You can register a trigger on a “parent” table so that when a document is deleted, the trigger deletes related documents. [Cascade deletes]

triggers.register("users", async (ctx, change) => {
  if (change.operation === "delete") {
    for await (const message of ctx.db
      .query("messages")
      .withIndex("owner", q => q.eq("owner", change.id))) {
      await ctx.db.delete(message._id);
    }
  }
});
#

This keeps your application code simple, but has the same size/time limits as any mutation; for very large fan‑outs you’ll need an async approach. [Cascade deletes]

3. Async / batched cleanup for large graphs

For large cascades (e.g. “tenant” or “profile” ID used across many tables), you generally:

  1. Do a soft delete or mark the root as deleted.
  2. Use an async process (actions, scheduled functions, or the Workflow component) to:
    • Query a batch of related docs by index.
    • Delete that batch.
    • Reschedule / continue until done.

Convex staff explicitly recommend using the Workflow component for robust cascading deletes because it tracks progress and retries automatically; each step is still bounded, but the workflow as a whole can run arbitrarily long. [Too many reads]

“If I were implementing cascading deletes today I would take a look at the Workflow component… Because of the guarantees… With workflows, workflow state is tracked and retries are automatic.” [Too many reads]

4. Using Convex Ents (library) for cascades

If you’re open to using a library, Convex Ents has built‑in deletion behaviors:

  • Default: cascading delete of related documents in a single transaction (good for small fan‑outs).
  • Soft deletion.
  • Scheduled deletion: soft delete first, then cascade over many scheduled mutations so no single mutation does too much work. [Ents deletes]

This gives you “no dangling references” without hand‑rolling all the logic.


So, while there’s no automatic DB‑level cascade, the recommended scalable approaches are:

#
  • Small relationships: delete related docs in the same mutation or via triggers.
  • Large relationships: soft delete + async cleanup, ideally via the Workflow component or Ents’ scheduled deletion.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: