Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
dde8c91
feat: categories of personal data, each with its own key, basis and r…
benatespina Nov 9, 2026
11dfdd6
feat(core): expectForgotten by categories, the key in force by person…
benatespina Nov 9, 2026
b2759e0
test: expectForgotten categories option typed
benatespina Nov 9, 2026
79f14b7
test: kill mutants in forget and resealing; one sealing order
benatespina Nov 9, 2026
7c83d6d
test(postgresql): adoption of keys from before categories
benatespina Nov 9, 2026
d8cfb31
feat: retentions in calendar months and years
benatespina Nov 9, 2026
2ae4ef6
feat(core): seal anew what a field moved to another category holds
benatespina Nov 9, 2026
dda570b
feat(core): end what a forgetting kept once no legal obligation keeps…
benatespina Nov 9, 2026
1bbb1f0
chore: changeset names the retained listing
benatespina Nov 9, 2026
b610cd0
test: races of the key sweeps
benatespina Nov 9, 2026
9fe28f8
refactor: one pattern for calendar retentions
benatespina Nov 9, 2026
e5d258e
fix(core): choose keys only for what a reseal opens; order marks star…
benatespina Nov 9, 2026
10d016b
test: walking marks again, and whom a moved command names
benatespina Nov 9, 2026
c646554
refactor: review findings on recategorizing
benatespina Nov 9, 2026
42ecfe2
Merge main into feat/personal-data-categories
benatespina Nov 10, 2026
5a7739b
feat(cloudflare): keep the keys of a forget ledger entry in the key's…
benatespina Nov 10, 2026
655edc9
test: a category's retention committed again, and the retained keys a…
benatespina Nov 10, 2026
173496b
fix: review findings on the integration with the forget ledger's inte…
benatespina Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/forget-intention-categories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@bounda-dev/core": minor
"@bounda-dev/cli": minor
"@bounda-dev/sqlite": minor
"@bounda-dev/postgresql": minor
"@bounda-dev/cloudflare": minor
---

The forget ledger's intention carries the categories a forgetting asks for, and the reconciliation commits again only those, never the person as a whole.

- The intention names the keys in force its scope covers, every one for `forget(person)`, those of the categories named for `forget(person, { categories })`, and the key a category's retention ends; a forgetting finds a key given after it in that scope and takes a new intention, and one committed again keeps such a key.
- `PersonalDataForgotten.fingerprints` names every key the forgetting covered, destroyed or retained.
- A forgetting of a category's retention is committed again only while that category's retention would still end its keys, and destroys only those.
- `bounda status` and `app.status().retention` give a line for each retention: the app's, which forgets persons as a whole, and each declared category's, with how many are past it and when the next comes due, and how many keys a forgetting kept that no legal obligation keeps any more (`RetentionStatus` becomes `{ categories, keptBeyond }`, each a `CategoryRetentionStatus`). It is `null` only when no schema marks personal data.
- Breaking for adapters: `PersonalKeyStore` gains `countDue`, `countRetained` and `releaseDueHolds`; a restore clears the holds of the keys past their retention too. The storage migration that keys each category is now version 3, after the subscriber failures, and the ledger's categories column version 4, after its reason and keys.
- The KV forget ledger keeps in the key's value the categories and keys of an entry too large for KV's metadata, and reads them, at once, for the entries it hands out alone; `planKvForgetLedgerExpiry` plans such a key with `value: null`, which `expireKvForgetLedger` and `bounda forget-ledger expire` read before writing it again, one at a time, leaving alone a key gone meanwhile.
- `PersonalDataForgotten.whole` is `false`, and the category is not among `categories`, when a forgetting committed again keeps a key given to the person after it.
16 changes: 16 additions & 0 deletions .changeset/personal-data-categories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@bounda-dev/core": minor
"@bounda-dev/cli": minor
"@bounda-dev/sqlite": minor
"@bounda-dev/postgresql": minor
"@bounda-dev/cloudflare": minor
---

Categories of personal data, each with its own key. `app/personal-data.ts` declares the categories with their legal basis (article 6 of the GDPR) and their retention, and `personal(schema, { category })` and `.personal({ category })` put a field in one; a field marked without one is in the default category, which behaves as every personal field did before. A misspelled category does not compile, and the app refuses to start on one.

- A person has a key per category. `forget(person)` forgets every category but those whose basis is `legal-obligation` while their retention runs: those are kept, restricted, and forgotten on their own when it ends. `forget(person, { categories })` forgets those alone, as when a consent is withdrawn; policies and processes take the same option.
- Each declared category is forgotten after its own retention, counted from the person's last activity; `runtime.personalData.retention` is the default category's, and forgets the person as a whole. Retentions take whole calendar months and years, `"18mo"`, `"6y"`, counted on the calendar, besides durations (`RetentionInput`); `ResolvedConfig.runtime.personalData.retentionMs` becomes `retention`.
- A field moved to another category is sealed anew under its new category's key by the backfill, everywhere it is stored; going back to earlier marks walks the stream again. What a forgetting kept is destroyed once its category is no longer a legal obligation's.
- `PersonalDataForgotten` gains `whole`, `categories`, `retained` and `expired`. The export names each field's category and every category's basis, retention and, for one kept, until when.
- Breaking for adapters: `PersonalKeyStore` keys by person and category (`get(person, category)`, `find`, `list`, `add(person, category, key)`, `remove(person, fingerprint)`, `retain`, `touch`, `listDue`, `holdDue`, `nextDueHoldEnd`, `listRetained`, `nextRetainedHoldEnd`), the snapshot, idempotency, scheduler and dead-letter stores' `forget` take the fingerprints to keep, `checkAppend` takes `keyOf`, `LoadResult.fingerprints` becomes `opened`, `StoredPerson` gains `position`, and forget ledger entries gain `categories`. The SQLite, PostgreSQL and Durable Object stores migrate their tables forward on start.
- CLI: `bounda forget <person> [--category <name>…]` and `bounda export <person>`; `bounda forgotten` lines carry `categories`.
5 changes: 4 additions & 1 deletion docs/src/content/docs/adapters/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,10 @@ reconciliation never wakes an object on its own, and with `keepFor` each key exp
itself; `bounda forget-ledger expire` gives the keys written before `keepFor` was set theirs.
Every forgetting writes its intention to KV before the object commits it, so one the object did not
get to commit, because it was evicted or its transaction failed, is committed at its next start;
with `strict`, a forgetting KV does not take fails and forgets nobody. See
with `strict`, a forgetting KV does not take fails and forgets nobody. An entry lives in its key's
metadata, which KV caps at 1024 bytes; the categories and keys of a forgetting with too many to fit
there, some thirty, go to the key's value, which a read fetches for the entries it hands out alone,
and `bounda forget-ledger expire` writes back as it was, leaving alone a key gone meanwhile. See
[backups](/guides/personal-data/#backups).

## One object per tenant
Expand Down
Loading
Loading