Jadpo is experimental and not ready for production. These docs follow the compiler's main branch as of 5 Nov 2026. What's built · Install 0.3.0
jadpo
ReferencePersonal data

DataPersonal data

Retention

On this page

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:

example
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:

example
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.