Skip to content

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.

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:

ProfileRegionAuthorityLanguagesTenant-Destroy-Grace
eu-dsgvoDACH (default)BlnBDI Berlinde, en30 days
swiss-dsg (extends eu-dsgvo)CHEDÖB Bernde, fr, it, en30 days
de-hr-dsgvo-hgb (extends eu-dsgvo)DE-HRLandes-Datenschutzbehördede60 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 dispatcher
r.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 };
},
});

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 gone
  • anonymize — PII columns nulled, row stays (multi-user-refs intact)
  • blockDelete — row stays untouched until keepFor expires; relevant for HR/Steuer-Aufbewahrungspflicht (de-hr-dsgvo-hgb sets 10y for invoices)

Example:

const invoiceEntity = createEntity({
fields: { ... },
retention: { keepFor: "10y", strategy: "blockDelete", reference: "createdAt" },
});

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:

ArticleHandler / CronWho
Art. 15user-data-rights:query:my-audit-logUser (account-wide event history)
Art. 15 + 20user-data-rights:write:request-exportUser (async ZIP + magic-link)
Art. 15 + 20GET /user-export/by-tokenanonymous (magic-link, multi-use within TTL)
Art. 15 + 20GET /user-export/by-job/:jobIdUser (session-auth, cross-tenant-same-user)
Art. 17user-data-rights:write:request-deletion + cancel-deletionUser
Art. 17run-forget-cleanupCron (after grace)
Art. 18user-data-rights:write:restrict-account + lift-restrictionAdmin / SystemAdmin
Operatoruser-data-rights:query:list-download-attemptsAdmin / DPO

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"]! },
});

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)