Skip to content

Retention

What HiLMS stores about a person, how long it stays, and which environment key moves it. Everything here is the current behaviour of the installed code, not a promise about a future version.

Read this together with requirements.md, which lists the same keys with their defaults.

Store What it is Retention Key What deletion does to it
users Name, email address, password hash, verification, two-factor secret and recovery codes Until the account is deleted — The row becomes a tombstone: name “Deleted user”, deleted-{id}@hilms.invalid, an unusable random password, no verification, no second factor, deleted_at stamped. The id survives so the trail below still points somewhere
social_accounts Google and Facebook identities with their encrypted tokens Until disconnected or the account is deleted — Deleted
passkeys Public keys, names and last use Until removed or the account is deleted — Deleted
sessions Session payloads with the signed-in user id SESSION_LIFETIME minutes (120) SESSION_LIFETIME Deleted. A session in a database backup expired long before anybody could read it, and its cookie cannot be forged without APP_KEY, which no backup holds
password_reset_tokens One row per reset request 60 minutes (auth.passwords.users.expire) — Deleted
personal-data-exports disk The archives a person asked for PERSONAL_DATA_EXPORT_DELETE_AFTER_DAYS days (5), removed nightly by personal-data-export:clean PERSONAL_DATA_EXPORT_DELETE_AFTER_DAYS Deleted
oauth_access_tokens, oauth_refresh_tokens, oauth_auth_codes Tokens issued to an API client (no user) and to an agent connected over MCP, which names the person who approved it One hour for an access token, 30 days for a refresh token; passport:purge clears the expired ones daily — A machine token is untouched, because it never names a person. Every token and refresh token of the deleted account is deleted: the agent acted as them, and with the account gone it acts as nobody

Learning records, which name a person only by id

Section titled “Learning records, which name a person only by id”
Store What it is Retention Key What deletion does to it
entitlements Who was granted which course, by whom, when, until when, and the external reference of the order Kept: it is the audit trail behind a purchase — Stays on the tombstone id
enrollments The seat derived from the entitlements, with the resume point Kept — Stays on the tombstone id
lesson_completions Which lessons were finished and when Kept — Stays on the tombstone id
quiz_attempts Answers, score and pass mark Kept — Stays on the tombstone id
course_instructor Which courses somebody teaches Kept — Stays on the tombstone id
notifications The panel bell: what an editor was told, and a link to it Until read and cleared by hand — Deleted with the account, because the rows hang off it
media_assets.uploaded_by Which member of staff uploaded a file to the media library Until the file is deleted from the library — Stays on the tombstone id. A library file is staff content, not personal data: it is not in the export and is not deleted with the account

None of these hold a name or an address. A tombstone plus its learning rows says “account 412 was granted this course on this date”, which is what a shop, an accountant or a court may need, and nothing more.

Store What it is Retention Key What deletion does to it
ai_generations What an editor asked an assistant for, what it answered and what it cost. The prompt holds lesson or course material and the name of the editor who asked, never a student keep_days under Settings, AI assistants (30) after the run finished, removed daily by model:prune --model=Generation Settings, AI assistants Deleted: the rows name the editor who asked and cascade with the account
activity_log The audit trail: who changed what ACTIVITYLOG_CLEAN_AFTER_DAYS days (365), removed nightly by activitylog:clean ACTIVITYLOG_ENABLED, ACTIVITYLOG_CLEAN_AFTER_DAYS Entries about the account are deleted, because they hold its old name and address. Entries the account caused stay: they name an id and a role
Pulse tables Slow requests, queries and jobs, with the user id, name and email of the user who was seen 7 days (pulse.recorders, PULSE_ENABLED) PULSE_ENABLED Nothing: the rows age out within a week. Set PULSE_ENABLED=false to record nothing
telescope_entries Full request, query and mail payloads Local only; telescope:prune daily TELESCOPE_ENABLED Nothing. Telescope is never enabled in production
The cache (Redis, and the cache table during a Redis outage) Snapshots of settings, menus, languages and permissions; the rate limits, whose keys name an address and an email being tried Rate limits for their window (a minute to an hour); snapshots until something they hold changes. The cache table is emptied by hilms:cache:settle within a minute of Redis coming back — Nothing: no key outlives its window
Horizon Job payloads in Redis 60 minutes for completed jobs, 7 days for failed ones (horizon.trim) — Nothing: the payloads age out
failed_jobs Payloads of jobs that failed 30 days (queue:prune-failed --hours=720) — Nothing
health_check_result_history_items Health check results 5 days (health.result_stores) — Nothing: no personal data
storage/logs Application logs LOG_DAILY_DAYS days (14) LOG_DAILY_DAYS Nothing
Backups Nightly database and media archives; media kept in a storage bucket is not in them, it is the provider’s to keep BACKUP_KEEP_ALL_DAYS 7, then daily for BACKUP_KEEP_DAILY_DAYS 16, weekly for BACKUP_KEEP_WEEKLY_WEEKS 8, monthly for BACKUP_KEEP_MONTHLY_MONTHS 4, and no yearly copies BACKUP_KEEP_*, BACKUP_S3_* Nothing directly. A deleted account is still in the archives until the last one holding it is cleaned up, which is at most about seven months

Mail carries names and addresses. MAIL_MAILER=log writes every message body into storage/logs, where the retention above is LOG_DAILY_DAYS and nobody expects personal data. Never leave MAIL_MAILER=log on an installation real people use. The production template sets smtp, and Settings → Mail in the panel overrides it.

Whichever transport is chosen, the provider behind it sees every address HiLMS writes to, and keeps its own log of that for its own retention period. HiLMS keeps no copy of a sent message.

The redirect field there (MAIL_ALWAYS_TO, then the panel value) is a pin for a staging installation: while it is filled, every message — password resets, verification, enrolment mail — goes to that one address instead of the person it names. That is what makes a staging copy safe to point at real data. It is equally a way of silently cutting every student off, so the page shows it in red for as long as it is set, and it must be empty on an installation real people use.

  • Backups taken before the deletion, until they are cleaned up.
  • Anything the installation exported by hand: an archive somebody downloaded, a dump taken for a migration, a copy of the database on a laptop.
  • Mail already delivered.

Nothing in HiLMS can reach those. An installation that promises a hard erasure has to handle them by hand.

Asking for the data and asking for the deletion

Section titled “Asking for the data and asking for the deletion”

A signed-in person does both from /account:

  • Your data builds an archive of every store above that names them (one JSON file per source plus a README.txt) and mails the link. One request per hour per account; the link works only while they are signed in and dies after PERSONAL_DATA_EXPORT_DELETE_AFTER_DAYS days.
  • Delete my account asks for the password (or, for an account that only ever signed in with a provider, for the address to be typed out) and deletes immediately, as described above.

Staff do the same from the panel: the users list carries “Export data” and “Delete account”; the console has hilms:user:delete.

HiLMS is MIT-licensed. No replicants were harmed in the writing of these books.