skills/imfa-solutions/skills/convex-cascading-delete-skill

convex-cascading-delete-skill

SKILL.md

Convex Cascading Delete

Automatic cascading deletion of related documents across Convex tables using the @00akshatsinha00/convex-cascading-delete component.

The component uses your existing Convex indexes to define relationships and provides both atomic and batched deletion modes. When you delete a parent document, all configured child documents are automatically removed, preventing orphaned records.

Installation

# 1. Install the package
npm install @00akshatsinha00/convex-cascading-delete

# 2. Install the Convex component
npx convex component install @00akshatsinha00/convex-cascading-delete

# 3. Verify — run dev and check for errors
npx convex dev

Setup — Three Steps

Step 1: Schema with indexes on foreign keys

Every foreign key field that participates in a cascade needs an index. Without the index, the component cannot efficiently look up child documents.

// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    name: v.string(),
    email: v.string(),
  }),

  posts: defineTable({
    title: v.string(),
    content: v.string(),
    authorId: v.id("users"),
  }).index("by_author", ["authorId"]),

  comments: defineTable({
    text: v.string(),
    postId: v.id("posts"),
    authorId: v.id("users"),
  })
    .index("by_post", ["postId"])
    .index("by_author", ["authorId"]),
});

Step 2: Define cascade rules

Create a dedicated file for your rules. Each rule says: "when a document in this table is deleted, also delete documents in the to table by querying the via index on the field that references the parent."

// convex/cascadeRules.ts
import { defineCascadeRules } from "@00akshatsinha00/convex-cascading-delete";

export const cascadeRules = defineCascadeRules({
  // Deleting a user cascades to their posts and comments
  users: [
    { to: "posts", via: "by_author", field: "authorId" },
    { to: "comments", via: "by_author", field: "authorId" },
  ],
  // Deleting a post cascades to its comments
  posts: [
    { to: "comments", via: "by_post", field: "postId" },
  ],
});

Rule structure:

{ to: "childTable", via: "indexName", field: "foreignKeyField" }
  • to — the child table containing documents to delete
  • via — the index name on the child table (must exist in schema)
  • field — the field in the child table that references the parent's _id

Step 3: Use CascadingDelete in mutations

// convex/mutations.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { CascadingDelete } from "@00akshatsinha00/convex-cascading-delete";
import { cascadeRules } from "./cascadeRules";
import { components } from "./_generated/api";

const cd = new CascadingDelete(components.convexCascadingDelete, {
  rules: cascadeRules,
});

export const deleteUser = mutation({
  args: { userId: v.id("users") },
  returns: v.object({
    users: v.number(),
    posts: v.number(),
    comments: v.number(),
  }),
  handler: async (ctx, { userId }) => {
    const summary = await cd.deleteWithCascade(ctx, "users", userId);
    // summary = { users: 1, posts: 5, comments: 23 }
    return summary;
  },
});

Deletion Modes

Mode Default Best for
atomic Yes Small-to-medium graphs (< 100 docs), strict consistency
batched No Large graphs (> 1000 docs), background cleanup
// Atomic (default) — all-or-nothing in a single transaction
const cd = new CascadingDelete(components.convexCascadingDelete, {
  rules: cascadeRules,
});

// Batched — processes deletions in chunks to avoid transaction limits
const cd = new CascadingDelete(components.convexCascadingDelete, {
  rules: cascadeRules,
  mode: "batched",
  batchSize: 500, // default is 100
});

Use batched mode when you hit "Transaction too large" errors — this means the relationship graph has too many documents for a single atomic transaction.

API Reference

defineCascadeRules(rules)

Defines the cascade configuration. Each key is a parent table name, each value is an array of { to, via, field } rules.

new CascadingDelete(component, options)

Option Type Default Description
rules CascadeRules required Output of defineCascadeRules()
mode "atomic" | "batched" "atomic" Deletion strategy
batchSize number 100 Docs per batch (batched mode only)
debug boolean false Enable verbose logging

cd.deleteWithCascade(ctx, tableName, documentId)

Deletes the document and all related documents per cascade rules. Returns a DeleteSummary (Record<string, number>) with counts of deleted documents per table.

Real-World Examples

Organization hierarchy

// Schema
organizations: defineTable({ name: v.string(), slug: v.string() }),
teams: defineTable({ name: v.string(), orgId: v.id("organizations") })
  .index("by_org", ["orgId"]),
members: defineTable({ userId: v.string(), teamId: v.id("teams"), role: v.string() })
  .index("by_team", ["teamId"]),
projects: defineTable({ name: v.string(), teamId: v.id("teams") })
  .index("by_team", ["teamId"]),

// Rules — deleting an org removes its teams, which removes members and projects
const cascadeRules = defineCascadeRules({
  organizations: [
    { to: "teams", via: "by_org", field: "orgId" },
  ],
  teams: [
    { to: "members", via: "by_team", field: "teamId" },
    { to: "projects", via: "by_team", field: "teamId" },
  ],
});

Multiple cascade paths from one table

const cascadeRules = defineCascadeRules({
  users: [
    { to: "posts", via: "by_author", field: "authorId" },
    { to: "comments", via: "by_author", field: "authorId" },
    { to: "teamMembers", via: "by_user", field: "userId" },
    { to: "likes", via: "by_user", field: "userId" },
    { to: "bookmarks", via: "by_user", field: "userId" },
  ],
});

Best Practices

  1. Always index foreign key fields — the component queries these indexes to find children. Without indexes, cascading deletes won't work efficiently.

  2. Keep cascade hierarchies clear and acyclic. The component handles circular references safely, but circular rules are confusing and usually indicate a design issue. Prefer a strict parent-child hierarchy: org -> teams -> members.

  3. Choose atomic mode by default. Only switch to batched when you actually hit transaction size limits. Atomic gives you all-or-nothing consistency.

  4. Log the deletion summary for auditing. The summary tells you exactly what was removed:

    const summary = await cd.deleteWithCascade(ctx, "users", userId);
    console.log("Cascade delete result:", summary);
    
  5. Consider soft deletes for audit trails. Add a deletedAt field and a separate deletionLogs table if you need to track what was deleted and by whom.

  6. Test cascade behavior. Use convex-test to verify the full chain works:

    import { convexTest } from "convex-test";
    const t = convexTest(schema);
    
    test("deleting user cascades to posts and comments", async () => {
      const userId = await t.run(async (ctx) =>
        ctx.db.insert("users", { name: "Test", email: "t@t.com" })
      );
      const postId = await t.run(async (ctx) =>
        ctx.db.insert("posts", { title: "P", content: "C", authorId: userId })
      );
      await t.run(async (ctx) =>
        ctx.db.insert("comments", { text: "Hi", postId, authorId: userId })
      );
    
      const summary = await t.mutation(api.mutations.deleteUser, { userId });
      expect(summary).toEqual({ users: 1, posts: 1, comments: 1 });
    });
    
  7. Monitor large cascades. If a single delete takes over 5 seconds, consider batched mode or restructuring your data model.

Troubleshooting

Error Cause Fix
"Index not found" The via index doesn't exist in schema Add .index("indexName", ["field"]) to the child table
"Transaction too large" Too many docs in one atomic delete Switch to mode: "batched"
Documents not deleted Wrong field name in rule Ensure field matches the actual field name in the child table schema
TypeScript errors Stale generated types Run npx convex dev or npx convex codegen

Enable debug logging for detailed cascade operation traces:

const cd = new CascadingDelete(components.convexCascadingDelete, {
  rules: cascadeRules,
  debug: true,
});

Client Usage

import { useMutation } from "convex/react";
import { api } from "../convex/_generated/api";

function DeleteUserButton({ userId }: { userId: Id<"users"> }) {
  const deleteUser = useMutation(api.mutations.deleteUser);

  return (
    <button onClick={async () => {
      const result = await deleteUser({ userId });
      console.log(`Deleted ${result.posts} posts, ${result.comments} comments`);
    }}>
      Delete User
    </button>
  );
}

Resources

Weekly Installs
1
GitHub Stars
1
First Seen
1 day ago
Installed on
windsurf1
amp1
cline1
openclaw1
adal1
opencode1