Compliance & DSGVO
Four features that together implement DSGVO Art. 15 (Auskunft) +
Art. 17 (Löschung mit Grace) + Art. 18 (Restriction) + Art. 20
(Portabilität) for any multi-tenant Kumiko app. The core value:
your domain entities plug into the pipeline with two hooks per
entity (export, delete) — Forget-Cron, Export-ZIP-Bau,
Magic-Link-Versand and Auth-Middleware-Guard come from the framework.
compliance-profiles
Section titled “compliance-profiles”Status: ✅ Stable
What: Region-specific defaults for grace periods, supervisory
authorities, notification languages, audit-log retention, sub-processor
contracts. Tenant-Admin picks one profile during onboarding; every
consumer feature reads compliance.forTenant(tenantId) and gets
profile-aware behaviour automatically.
Profiles shipped:
| Profile | Region | Authority | Languages | Tenant-Destroy-Grace |
|---|---|---|---|---|
eu-dsgvo | DACH (default) | BlnBDI Berlin | de, en | 30 days |
swiss-dsg (extends eu-dsgvo) | CH | EDÖB Bern | de, fr, it, en | 30 days |
de-hr-dsgvo-hgb (extends eu-dsgvo) | DE-HR | Landes-Datenschutzbehörde | de | 60 days (HR-Override) |
minimal-no-region | — | (migration edge-case) | — | — |
Per-tenant overrides via override.userRights.gracePeriod={ days: N }
keep the rest of the profile inherited.
Example:
import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
await runDevApp({ features: [createComplianceProfilesFeature(), myFeature],});
// Tenant-Admin sets profile via the dispatcherr.queryHandler({ name: "settings", handler: async (_q, ctx) => { const profile = await ctx.queryAs(ctx.user, "compliance-profiles:query:for-tenant", {}); return { gracePeriodDays: profile.userRights.gracePeriod.days }; },});data-retention
Section titled “data-retention”Status: ✅ Stable
What: Retention policy resolver per Entity in 3 layers (Entity-
Default → Tenant-Preset → Tenant-Override). Cleanup-Cron deletes or
anonymizes rows after keepFor expires; supports blockDelete for
legal-hold periods (HGB, Steuer-Aufbewahrungspflicht).
How it works: Add retention: { keepFor: "90d", strategy: "hardDelete", reference: "createdAt" }
to createEntity({...}). The resolver picks the most specific policy;
cleanup-runner iterates due rows in batches.
Strategies:
hardDelete— row goneanonymize— PII columns nulled, row stays (multi-user-refs intact)blockDelete— row stays untouched untilkeepForexpires; relevant for HR/Steuer-Aufbewahrungspflicht (de-hr-dsgvo-hgbsets 10y for invoices)
Example:
const invoiceEntity = createEntity({ fields: { ... }, retention: { keepFor: "10y", strategy: "blockDelete", reference: "createdAt" },});user-data-rights
Section titled “user-data-rights”Status: ✅ Stable
What: Full DSGVO pipeline — request-export, request-deletion + cancel, restrict-account + lift, my-audit-log, list-download-attempts (DPO operator-query). Plus the async export-job worker (ZIP streaming + storage upload + signed magic-link with SHA-256 hash + edge-rate-limit
- 90d brute-force-detection-audit).
How it works: Domain features hook into EXT_USER_DATA with two
hooks per entity:
defineFeature("notes", (r) => { r.requires("user-data-rights"); r.useExtension(EXT_USER_DATA, "note", { export: async (ctx) => ({ entity: "note", rows: await ctx.db.select().from(notesTable) .where(and(eq(notesTable.tenantId, ctx.tenantId), eq(notesTable.authorId, ctx.userId))), }), delete: async (ctx, strategy) => { const where = and(eq(notesTable.tenantId, ctx.tenantId), eq(notesTable.authorId, ctx.userId)); if (strategy === "anonymize") { await ctx.db.update(notesTable).set({ authorId: null }).where(where); } else { await ctx.db.delete(notesTable).where(where); } }, });});That’s it. Forget-Cron iterates all EXT_USER_DATA providers,
calls the strategy from retention.policyFor, anonymizes the user
row. Export-Job builds the ZIP, sends a Magic-Link.
Endpoints shipped:
| Article | Handler / Cron | Who |
|---|---|---|
| Art. 15 | user-data-rights:query:my-audit-log | User (account-wide event history) |
| Art. 15 + 20 | user-data-rights:write:request-export | User (async ZIP + magic-link) |
| Art. 15 + 20 | GET /user-export/by-token | anonymous (magic-link, multi-use within TTL) |
| Art. 15 + 20 | GET /user-export/by-job/:jobId | User (session-auth, cross-tenant-same-user) |
| Art. 17 | user-data-rights:write:request-deletion + cancel-deletion | User |
| Art. 17 | run-forget-cleanup | Cron (after grace) |
| Art. 18 | user-data-rights:write:restrict-account + lift-restriction | Admin / SystemAdmin |
| Operator | user-data-rights:query:list-download-attempts | Admin / DPO |
user-data-rights-defaults
Section titled “user-data-rights-defaults”Status: ✅ Stable
What: Default EXT_USER_DATA hooks for the core entities user
and fileRef — anonymizes user-row with sentinel email
(deleted-<id>@anonymized.invalid), nulls displayName and
passwordHash, deletes file rows + storage binaries on delete /
keeps row + nulls insertedById on anonymize.
Why it’s optional: App-authors with custom user-anonymize policies (e.g. soft-delete with retention) can leave this feature out and register their own hooks. 95% of apps just mount it.
Example:
await runDevApp({ features: [ createComplianceProfilesFeature(), createDataRetentionFeature(), createUserDataRightsFeature(), createUserDataRightsDefaultsFeature(), // covers user + fileRef notesFeature, // your domain ], auth: { jwtSecret: process.env["JWT_SECRET"]! },});See also
Section titled “See also”- Recipe:
samples/recipes/user-data-rights/— minimal Notes-domain withEXT_USER_DATA-Hooks, 3 tests - Sample-App:
samples/apps/user-data-rights-demo/— runnable dev-server with todos-domain - Compliance-Operator-Guide:
packages/bundled-features/src/user-data-rights/COMPLIANCE.md— Verarbeitungsverzeichnis material + AVV-TOM list + Datenschutzerklärung snippets - Cross-Data-Matrix Test:
packages/bundled-features/src/user-data-rights/__tests__/cross-data-matrix.integration.ts— pins the 3-provider integration (user + fileRef + custom-domain)
What this framework does NOT cover
Section titled “What this framework does NOT cover”App-author responsibility (legal, not technical):
- Selecting + AVV with concrete storage/email providers
- Data-breach reporting to supervisory authority
- DSFA (Datenschutz-Folgenabschätzung) — the framework provides TOM inputs, the assessment is yours
- Cookie-consent layer (frontend / separate feature)
- Tenant-lifecycle destroy (Sprint 5 — separate feature)