user-data-rights
Implements GDPR Art. 15 (access / my-audit-log query), Art. 17 (erasure / request-deletion + cancel-deletion, plus the anonymous email-verified request-deletion-by-email + confirm-deletion-by-token flow for lockout-safe self-service, + cron cleanup with grace period), Art. 18 (restriction / restrict-account + lift-restriction), and Art. 20 (portability / async request-export → ZIP via file-foundation, Magic-Link download) as first-class HTTP handlers and cron jobs. Each domain feature opts in by calling r.useExtension(EXT_USER_DATA, "<entity>", { export, delete }) — the feature then orchestrates the export and forget pipelines across all registered hooks automatically. When mail-foundation and a mail-transport-* are mounted, it also sends the four GDPR notifications (export ready/failed, deletion requested/executed) itself with no app callback code, rendered in each recipient’s locale. Requires user, data-retention, compliance-profiles, and sessions.
Quick example
Section titled “Quick example”From recipes-user-data-rights — the smallest working mount:
// User-Data-Rights — Recipe//// Wie eine App-Domain DSGVO-Pipeline integriert: pro Entity einen// (export, delete)-Hook ueber EXT_USER_DATA. Forget-Cron, Export-ZIP-Bau// und Magic-Link-Versand kommen vollstaendig aus user-data-rights —// App-Author schreibt nur die zwei Hooks pro Entity.//// Demo-Domain: minimaler Note-Service. Note hat (id, tenantId, authorId,// title, body). Pinst:// 1. Hook export liefert Note-Rows als JSON-Snippet ins Export-Bundle.// 2. Hook delete entfernt Note-Rows beim Forget-Cleanup-Cron.//// Was nicht im Recipe ist (siehe samples/apps/user-data-rights-demo// fuer eine vollstaendige App):// - Strategy-aware delete (anonymize vs hardDelete)// - HTTP-Endpoints fuer create/list// - Compliance-Profile-Wiring + Cron-Scheduling
import { deleteMany, selectMany, updateMany } from "@cosmicdrift/kumiko-framework/bun-db";import { deriveEntityTableMeta } from "@cosmicdrift/kumiko-framework/db";import { createEntity, createTextField, defineFeature, EXT_USER_DATA, type UserDataDeleteHook, type UserDataExportHook,} from "@cosmicdrift/kumiko-framework/engine";
export const noteEntity = createEntity({ table: "store_notes", idType: "uuid", fields: { authorId: createTextField({}), title: createTextField({ required: true, maxLength: 200 }), body: createTextField({ maxLength: 4000 }), },});
// Plain EntityTableMeta, NOT a branded EntityTable: store_notes is a deliberate// unmanaged direct-write store (r.storeTable below), so the forget hook may// updateMany/deleteMany it directly — the meta carries no executor-only brand.export const notesTable = deriveEntityTableMeta("note", noteEntity, { source: "unmanaged" });
export const notesFeature = defineFeature("notes", (r) => { r.requires("user-data-rights"); // store_notes is a direct-write store: the forget hook below `updateMany`/ // `deleteMany`s rows WITHOUT emitting lifecycle events. r.entity would make // it a rebuildable implicit projection whose replay finds zero note events // and wipes the table (or un-forgets anonymized rows) on the next rebuild // (#498). r.storeTable keeps the DDL, opts out of implicit rebuild. r.storeTable(notesTable, { reason: "read_side.notes_direct_write", });
const exportNotes: UserDataExportHook = async (ctx) => { const rows = await selectMany(ctx.db, notesTable, { tenantId: ctx.tenantId, authorId: ctx.userId, }); if (rows.length === 0) return null; return { entity: "note", rows: rows.map((row) => ({ id: String(row.id), title: row.title ?? "", body: row.body ?? "", })), }; };
const deleteNotes: UserDataDeleteHook = async (ctx, strategy) => { const where = { tenantId: ctx.tenantId, authorId: ctx.userId }; if (strategy === "anonymize") { await updateMany(ctx.db, notesTable, { authorId: null }, where); } else { await deleteMany(ctx.db, notesTable, where); } };
r.useExtension(EXT_USER_DATA, "note", { export: exportNotes, delete: deleteNotes, });});📄 On GitHub: samples/recipes/user-data-rights/src/feature.ts
Live preview
Section titled “Live preview”
How it fits
Section titled “How it fits”What this feature needs to run (Requires, top) and the write commands it provides (Provides, bottom).
flowchart TB
n_user_data_rights["user-data-rights"]
subgraph how_reqs["Requires"]
n_user["user"]
n_data_retention["data-retention"]
n_compliance_profiles["compliance-profiles"]
n_sessions["sessions"]
end
n_uses_compliance_forTenant(("compliance.forTenant"))
n_uses_retention_policyFor(("retention.policyFor"))
n_uses_sessions_revokeAllForUser(("sessions.revokeAllForUser"))
subgraph how_provides["Provides"]
n_cmd_user_data_rights_write_cancel_deletion(["cancel-deletion"])
n_cmd_user_data_rights_write_confirm_deletion_by_token(["confirm-deletion-by-token"])
n_cmd_user_data_rights_write_lift_restriction(["lift-restriction"])
n_cmd_user_data_rights_write_request_deletion(["request-deletion"])
n_cmd_user_data_rights_write_request_deletion_by_email(["request-deletion-by-email"])
n_cmd_user_data_rights_write_request_export(["request-export"])
n_cmd_more(["+2 more"])
end
n_user --> n_user_data_rights
n_data_retention --> n_user_data_rights
n_compliance_profiles --> n_user_data_rights
n_sessions --> n_user_data_rights
n_uses_compliance_forTenant -->|uses| n_user_data_rights
n_uses_retention_policyFor -->|uses| n_user_data_rights
n_uses_sessions_revokeAllForUser -->|uses| n_user_data_rights
n_user_data_rights --> n_cmd_user_data_rights_write_cancel_deletion
n_user_data_rights --> n_cmd_user_data_rights_write_confirm_deletion_by_token
n_user_data_rights --> n_cmd_user_data_rights_write_lift_restriction
n_user_data_rights --> n_cmd_user_data_rights_write_request_deletion
n_user_data_rights --> n_cmd_user_data_rights_write_request_deletion_by_email
n_user_data_rights --> n_cmd_user_data_rights_write_request_export
n_user_data_rights --> n_cmd_more
Provides — write commands this feature registers (dispatch them through the command bus):
user-data-rights:write:cancel-deletionuser-data-rights:write:confirm-deletion-by-tokenuser-data-rights:write:lift-restrictionuser-data-rights:write:request-deletionuser-data-rights:write:request-deletion-by-emailuser-data-rights:write:request-exportuser-data-rights:write:restrict-accountuser-data-rights:write:run-forget-cleanup
Getting started
Section titled “Getting started”Start with recipes-user-data-rights for a step-by-step walkthrough with runnable code and integration tests.
Dependencies
Section titled “Dependencies”- Requires:
user,data-retention,compliance-profiles,sessions - Activation: always on (not toggleable)
Configuration
Section titled “Configuration”Per-tenant config keys, set via the tenant-admin UI or a seed. 🔒 = encrypted at rest.
| Key | Type | Default | Scope | Who can write | Who can read |
|---|---|---|---|---|---|
tenant-model | select (single-user | multi-user) | multi-user | system | system | TenantAdmin, Admin, SystemAdmin |
Extensions & cross-feature APIs
Section titled “Extensions & cross-feature APIs”- Exposes API:
userDataRights.runForget,userDataRights.runExport - Uses API:
compliance.forTenant,retention.policyFor,sessions.revokeAllForUser