Retention
Personal data needs an end date: GDPR's storage limitation means you keep
it only as long as you need it. Nothing expires unless you say so: a value
or record without a retention: is kept until your code deletes it or the
person is erased. A retention: block gives the end date, in the same
after: and since: shape as a lifecycle's purge:
(Lifecycle): how long to keep it, after:,
counted from one of the record's Instant fields, since:.
On a field
The value is cleared when its time is up, and the record stays. Use it for a value you need only for a while, such as the address a visitor signed up from, kept for 30 days for fraud checks:
config { signup_ip_retention: Duration { default: 30d }}entity Visitor { id: Uuid signup_ip: IpAddress? { personal: PersonalData.online retention: { after: config.signup_ip_retention since: created_at } } created_at: Instant { generated: Generated.on_create } person: true}
Once a visitor signed up more than 30 days ago, signup_ip becomes
none, whether a lifecycle hides the visitor or not. Clearing it is a
change to the record, so its Generated.on_change fields take the time
of the run that cleared it.
The field must be one that can be none: its type ends in ?
(Values that may be missing),
or retention could never clear it
(data.retention_field_required).
It can't be a field the entity's lifecycle owns
or reads, such as the deleted_at its visible: tests, because clearing
it would show a deleted record again or undo what a transition set
(data.retention_lifecycle_field).
On an entity
The record is deleted, with whatever cascades from it. Use it for invoices, as in Erasure, or for accounts inactive for two years. Here an invoice goes six years after it was issued, and its lines go with it:
config { invoice_retention: Duration { default: 2192d }}type InvoiceLineText = Text { max_length: 200}entity Invoice { id: Uuid number: InvoiceNumber issued_at: Instant { generated: Generated.on_create } retention: { after: config.invoice_retention since: issued_at }}entity InvoiceLine { id: Uuid invoice_id: Invoice.id { on_delete: OnDelete.cascade } text: InvoiceLineText}
A record is deleted whether a lifecycle hides it or not, and so is every
record that cascades from it, records with a lifecycle among them, since
retention, like purge, is an operation the compiler generates. References
that are OnDelete.cleared are set to none, as for any delete
(References and on_delete).
A record an OnDelete.restricted reference holds, directly or through
what deleting it cascades to, can't be deleted, so its retention is an
error (data.retention_held): say
what deleting it does to the record that holds it.
An entity that someone signs in as, the user: or service: of
principal { }, goes with its sign-in state when its
retention ends, or another record's retention cascades to it: its
sessions and API tokens, service keys, passkeys and sign-in links are
deleted in the same statement, so nobody can sign in as them again.
A File field's retention clears the field, and a record's deletes the
record, cascades included; either way the file's bytes are deleted once
no record holds them
(Deleting and erasing).
Where the periods come from
The periods are settings in config.jadpo
(Settings), so they
are set per environment and appear in the report. after: names a
Duration setting that isn't secret, never a duration written in place
(data.retention_period). A run uses
the value the app started with, and a period that isn't above zero
deletes and clears nothing: the run fails instead.
since: names one of the record's Instant fields, bare, as a lifecycle
names them (data.retention_since).
A record whose since: field is none isn't counted yet, so it is kept
until the field has a value and that time is past.
When it runs
The compiler's built-in purge schedule
applies retention, every hour, so an entity with a retention:, its own
or a field's, has a schedule such as Invoice.purge even without a
lifecycle. A value or record goes at the first run once its since: is
longer ago than after:. Each run applies the entity's retention first,
then its lifecycle's purge:, then each field's retention, in small
batches, each logged, oldest first; the next hour's run carries on with
what one leaves. Its run record counts the records it deleted and the
values it cleared, and it emits no events.
Retention changes the database only. An event that was emitted holding a value keeps its copy until the runtime removes the event, once every subscriber has had it and a day has passed since it was emitted (Events), and a backup keeps what it held until it expires on the hosting platform's schedule.
No test runs retention, as none runs purge: the purge schedule has no name a test can write, and moving a test's clock on runs no schedule (Schedule tests). It is the compiler's own operation, proven by the compiler's live tests on both executors.
A person entity, or an entity holding marked fields, with no retention and no purge is listed in the report as "kept until erased".
Markdown for agents: /docs/retention.md.Built from Jadpo main at aee2e8f.