- Guides
- Recipes
Cascading delete
Delete a record and all related records across collections.
Trigger: xrpc.procedure:<nsid>. Needs: happyview-db, happyview-record.
local db = require("happyview.db")local record = require("happyview.record")
function handle(input, ctx) if not input.uri then return { error = "UriRequired", message = "uri is required" } end
local row = db.get(input.uri) if not row then return { error = "RecordNotFound", message = "no record at that uri" } end
if row.did ~= ctx.caller_did then return { error = "NotOwner", message = "you can only delete your own records" } end
-- Find the caller's comments that reference this URI local comments = db.records("xyz.statusphere.comment") :where("postUri", "=", input.uri) :did(ctx.caller_did) :limit(100) :run()
local to_delete = { input.uri } for _, comment in ipairs(comments.records) do to_delete[#to_delete + 1] = comment.uri end
for _, uri in ipairs(to_delete) do record.delete(uri) end
return { deleted = #to_delete }endimport db from "happyview.db";import record from "happyview.record";
export default async function handle(input, ctx) { if (!input.uri) { return { error: "UriRequired", message: "uri is required" }; }
const row = await db.get(input.uri); if (!row) { return { error: "RecordNotFound", message: "no record at that uri" }; }
if (row.did !== ctx.caller_did) { return { error: "NotOwner", message: "you can only delete your own records" }; }
// Find the caller's comments that reference this URI const comments = await db.records("xyz.statusphere.comment") .where("postUri", "=", input.uri) .did(ctx.caller_did) .limit(100) .run();
const toDelete = [input.uri, ...comments.records.map((comment) => comment.uri)];
for (const uri of toDelete) { await record.delete(uri); }
return { deleted: toDelete.length };}import db from "happyview.db";import record from "happyview.record";
export default async function handle(input: { uri?: string }, ctx: { caller_did: string }) { if (!input.uri) { return { error: "UriRequired", message: "uri is required" }; }
const row = await db.get(input.uri); if (!row) { return { error: "RecordNotFound", message: "no record at that uri" }; }
if (row.did !== ctx.caller_did) { return { error: "NotOwner", message: "you can only delete your own records" }; }
// Find the caller's comments that reference this URI const comments = await db.records("xyz.statusphere.comment") .where("postUri", "=", input.uri) .did(ctx.caller_did) .limit(100) .run();
const toDelete = [input.uri, ...comments.records.map((comment) => comment.uri)];
for (const uri of toDelete) { await record.delete(uri); }
return { deleted: toDelete.length };}How it works
Section titled “How it works”- Check the primary record exists with
db.getand belongs to the caller. Return a structured error if it doesn’t. Each error name (UriRequired,RecordNotFound,NotOwner) must be declared underdefs.main.errorsin the lexicon, as in"errors": [{ "name": "NotOwner" }], for the response to be a 400; an undeclared one is sent as a 200. - Query for related records: comments by the same user whose
postUrifield is the primary record’s URI.wherefilters on a field of the stored body, so the match happens in the database rather than in the script. - Delete everything with
record.delete. Each call removes the record from the user’s PDS and the local index.
The DPoP proof is signed per request; an app built on the HappyView SDK gets this from its session.
const response = await fetch("http://127.0.0.1:3000/xrpc/xyz.statusphere.deletePost", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, Authorization: `DPoP ${ACCESS_TOKEN}`, DPoP: DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ uri: "at://did:plc:abc/xyz.statusphere.post/abc123", }),});const data = await response.json();const response = await fetch("http://127.0.0.1:3000/xrpc/xyz.statusphere.deletePost", { method: "POST", headers: { "X-Client-Key": CLIENT_KEY, Authorization: `DPoP ${ACCESS_TOKEN}`, DPoP: DPOP_PROOF, "Content-Type": "application/json", }, body: JSON.stringify({ uri: "at://did:plc:abc/xyz.statusphere.post/abc123", }),});const data = await response.json();let response = client .post("http://127.0.0.1:3000/xrpc/xyz.statusphere.deletePost") .header("X-Client-Key", client_key) .header("Authorization", format!("DPoP {access_token}")) .header("DPoP", dpop_proof) .json(&serde_json::json!({ "uri": "at://did:plc:abc/xyz.statusphere.post/abc123" })) .send() .await?;body := `{ "uri": "at://did:plc:abc/xyz.statusphere.post/abc123" }`req, _ := http.NewRequest("POST", "http://127.0.0.1:3000/xrpc/xyz.statusphere.deletePost", bytes.NewBufferString(body))req.Header.Set("X-Client-Key", clientKey)req.Header.Set("Authorization", "DPoP "+accessToken)req.Header.Set("DPoP", dpopProof)req.Header.Set("Content-Type", "application/json")resp, err := http.DefaultClient.Do(req)curl -X POST http://127.0.0.1:3000/xrpc/xyz.statusphere.deletePost \ -H "X-Client-Key: $CLIENT_KEY" \ -H "Authorization: DPoP $ACCESS_TOKEN" \ -H "DPoP: $DPOP_PROOF" \ -H "Content-Type: application/json" \ -d '{ "uri": "at://did:plc:abc/xyz.statusphere.post/abc123" }'{ "deleted": 4}Use case
Section titled “Use case”Use a cascading delete when your data model has parent-child relationships across collections. For example, deleting a post should also clean up its comments, reactions or metadata records.
This only deletes records in the caller’s own repo, because a record write can’t reach another account’s repo. If the related records could have more than 100 matches, paginate through all of them before deleting.
© HappyProto

