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.
Personal data an account holds
Section titled “Personal data an account holds”| 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.
Operational stores
Section titled “Operational stores”| 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.
What a deletion does not reach
Section titled “What a deletion does not reach”- 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 afterPERSONAL_DATA_EXPORT_DELETE_AFTER_DAYSdays. - 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.