Skip to content

feat: categories of personal data, each with its own key, basis and retention - #263

Merged
benatespina merged 18 commits into
mainfrom
feat/personal-data-categories
Nov 10, 2026
Merged

benatespina merged 18 commits into
mainfrom
feat/personal-data-categories

Conversation

@benatespina

@benatespina benatespina commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Categories of personal data, each with its own key, legal basis and retention (GDPR articles 6, 5(1)(e) and 17(3)). A person now holds one key per category instead of one key in all.

What changes

  • Declaration. app/personal-data.ts exports categories, satisfies PersonalData.Categories: each with its basis (the six of article 6) and its retention. The generator reads it, writes its +types, registers the names (personalCategories) and checks the file (PersonalDataDeclaration). personal(schema, { category }) and .personal({ category }) put a field in one; a misspelled category does not compile, in personal, .personal or forget. The app refuses to start on the same. A field without a category is in default, whose retention is runtime.personalData.retention: an app without the file behaves as before.
  • Keys. One key in force per person and category, named by person and fingerprint. Envelopes name their category unless it is the default one.
  • Forgetting. forget(person) destroys every category except legal-obligation ones whose retention, counted from the person's real last activity, has not run out. Those are retained: no longer in force, still readable, and destroyed by the retention when their time ends (expired). forget(person, { categories }) withdraws a consent; policies and processes take the same option. Snapshots, remembered rejections, scheduled commands and dead letters go when they name the person under a key no longer in force. Error text is sealed under every key the person holds, one inside the other. PersonalDataForgotten gains whole, categories, retained and expired.
  • Retention. Each declared category is swept by its keys' last activity (listDue), with its own holds; the app's retention still forgets the person as a whole.
  • Export. Each record names the category of each field; the export describes every category's basis, retention and, when retained, until when (article 15(1)).
  • Ledger and CLI. Ledger entries keep the categories a forgetting asked for (migration 2 of the forget-ledger scope; KV metadata). New bounda forget <person> [--category …] and bounda export <person>; bounda forgotten lines carry categories.
  • Migration 2 of storage on SQLite, the Durable Object and PostgreSQL, idempotent: existing keys go to default, last active when their person was; earlier PersonalDataForgotten become whole forgettings of default.
  • Calendar retentions. Retentions take whole calendar months and years, "18mo", "6y", in categories and in runtime.personalData.retention (RetentionInput; ResolvedConfig...retentionMs becomes retention). A period ends on the same day and time; a day the month lacks ends at the very end of the month's last day, so it is never cut short, and the sweep's cutoff is its exact inverse.
  • Changing a category. A field moved to another category, to or from the default one included, is sealed anew under its new category's key by the backfill in events, process states, scheduled commands and dead letters, as the forgettings since say; snapshots of the stream and rejections remembered under the old key go. Going back to earlier marks walks the stream again. A retention change applies at once; a key a forgetting kept is destroyed once its category is no longer a legal obligation's (listRetained, expired).
  • A4 follow-up. expectForgotten(app, id, { categories }), and every key the person still holds (in force or retained) as theirs.
  • Breaking for adapter authors: PersonalKeyStore is keyed by person and category, the stores' forget take the fingerprints to keep, checkAppend takes keyOf, LoadResult.fingerprints is opened, StoredPerson gains position, ForgetLedgerEntry gains categories. Changeset minor.

Docs: personal data guide (Categories section, forgetting, export, retention, keys, ledger, tests), CLI, conventions, configuration and limitations. The "invoices six years, profile thirty days" limit is gone.

Checks

  • pnpm check with Docker green (3517 tests; timeouts under a load average of 140 rerun green alone), but astro check, which fails locally for an environment reason unrelated to this change.
  • Stryker on the changed files: core ≥90 % per file (survivors equivalent), cli 96 %, postgresql 91 %, cloudflare 92 %.
  • /code-review high: 4 findings, 3 fixed (new keys start from the person's last activity; a forgetting without whole counts as whole; error text read under a key kept since goes under a disposable key), 1 left (the memory key store scans every key, as the other memory stores do).

Left out, as follow-up pieces

  • Read-model columns of a retained category the person writes again after coming back: the expiry cannot tell old values from new ones in the clear (C1 seals them under the key).
  • Leaving retained rows out of queries is B3; the mark is persisted here (retained_since).

Integrates B4 (#260), A2b (#259) and B2 (#261): the forget ledger's intention
carries the categories a forgetting asks for and the reconciliation commits
again only those; the categories migrations follow B4's and B2's in each scope;
bounda status reports each retention.
@benatespina
benatespina merged commit e6c5c5a into main Nov 10, 2026
10 checks passed
@benatespina
benatespina deleted the feat/personal-data-categories branch November 10, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant