Guides CRUD operations for API resources with cascading dependencies, descriptive validation, and orphan prevention. Use when adding delete/remove operations, creating validation logic, building resources that depend on other resources, or when the user mentions "cascade delete", "orphan records", "duplicate detection", "validation errors", "resource cleanup", or "rollback on failure".
Patterns for building reliable CRUD operations in Tambo Cloud.
CONFLICT for duplicate name errors in skills but BAD_REQUEST for the same pattern in MCP servers. Always use CONFLICT for duplicates.23505. Catch it and map to a domain-specific exception rather than letting the raw DB error propagate.externalSkillMetadata for that provider must have their metadata cleared, or they'll reference a stale key.Promise.allSettled (not Promise.all) for batch external API calls -- partial failures need cleanup, not an all-or-nothing abort.Default to onDelete: "cascade" in the schema. Only use manual transaction cascades when deletion requires external API calls, metadata cleanup, or cross-reference logic.
// packages/db/src/schema.ts
export const skills = pgTable("skills", {
id: text("id").primaryKey(),
projectId: text("project_id")
.notNull()
.references(() => projects.id, { onDelete: "cascade" }),
// ...
});
When deletion requires cleanup beyond FK cascades (external APIs, metadata, cross-references), wrap in a transaction:
// packages/db/src/operations/project.ts
export async function deleteProject(db: HydraDb, id: string): Promise<boolean> {
return await db.transaction(async (tx) => {
await tx
.delete(schema.providerKeys)
.where(eq(schema.providerKeys.projectId, id));
await tx.delete(schema.apiKeys).where(eq(schema.apiKeys.projectId, id));
await tx
.delete(schema.projectMembers)
.where(eq(schema.projectMembers.projectId, id));
const deleted = await tx
.delete(schema.projects)
.where(eq(schema.projects.id, id))
.returning();
return deleted.length > 0;
});
}
Reference: packages/db/src/operations/project.ts lines 255-278
When a resource is replaced (not deleted), clear dependent metadata so dependents re-sync under the new resource:
// apps/web/server/api/routers/project.ts - addProviderKey
// When replacing a provider key, clear skill metadata for that provider
const skills = await operations.listSkillsForProject(ctx.db, projectId);
await Promise.all(
skills
.filter((s) => s.externalSkillMetadata?.[providerName])
.map(async (s) => {
const { [providerName]: _, ...remaining } = s.externalSkillMetadata ?? {};
return operations.updateSkill(ctx.db, {
projectId,
skillId: s.id,
externalSkillMetadata: remaining,
});
}),
);
Reference: apps/web/server/api/routers/project.ts lines 863-880
onDelete: "cascade" in the schemaEvery error must say what went wrong and what to do instead. Include the specific value that failed and an example of what's valid.
// Zod: include format example