Data Retention Policy
Undergrove · Internal policy, published for transparency Owner: Reid Brenza · Last reviewed: 31 August 2026 · Review annually
Required in writing by the amended COPPA Rule. Published because a retention policy nobody can read isn't much of a commitment.
Principle
Keep it only as long as it's doing something useful, then delete it. Where a law sets a minimum, that governs. Where nothing does, the answer is the shortest period that still works.
Schedule
Enforcement: retention is applied by an automated daily sweep. First run: 2026-08-12.
| Category | Retention | Why |
|---|---|---|
| Account (email, hashed password, DOB) | Until deletion | Needed to run the account |
| Academic profile and question answers | Until deletion | Needed to match |
| Sensitive attributes (18+ only, optional) | Until deletion or withdrawal | Needed for eligibility on some awards |
| Writing samples (text only) | Until deletion | Voice matching for the user's own drafts |
| Drafts | Until deletion | The user's work |
| Saved and matched scholarships | Until deletion | The user's list |
| Search and match history | 24 months | Product quality; no value beyond that |
| AI usage records (tokens, cost, model — no content) | 24 months, then user_id anonymised |
Cost accounting and margin |
| Deep-search job record — profile snapshot used to run a paid scan | 30 days after the scan settles, then the snapshot is cleared; the record itself (status, cost) is kept | No reader left once the scan is done — the live profile lives in your account, not in the snapshot; cost history stays for accounting |
| Scan progress checkpoints (in-progress deep-search state) | 30 days | Pure scratch state — the resume window itself is measured in hours, not days |
| Purchase records | 7 years | Tax records and chargeback windows |
| Server and application logs | Governed by our hosting provider's own platform log retention (currently a matter of days to weeks, depending on plan) — see Render's published retention periods | Debugging and security; this data never enters our own database |
| Parental consent email records (kind, status, timestamps) | Until the account is erased, then the recipient address is anonymised | Evidence a notice was or wasn't delivered — see "What deletion actually does" below |
| Under-13 signup attempts | Never stored | COPPA — see below |
| Scholarship corpus (not personal data) | Indefinite | The product |
| Aggregate operational counts (all-time signups, searches — no data about any individual) | Indefinite | Not personal data — same basis as the corpus row above; see "Aggregate counts" below |
Under-13 attempts are not retained
When the age screen returns a date of birth under 13, nothing is written. No user row, no email, no date of birth, no blocked-users list.
Keeping a record of a blocked child in order to enforce the block would mean collecting personal information from a child under 13 for the purpose of not collecting personal information from a child under 13. We don't do it.
What deletion actually does
erase_user() performs a hard delete, not a soft flag:
- account, profile, question answers, sensitive attributes — deleted
- writing samples, drafts, saved scholarships — deleted
- scan progress checkpoints — deleted; pure in-progress scratch state, not needed once an account is gone
- AI usage records —
user_idset to null, the rows survive as anonymous cost data - deep-search job records —
user_idset to null and the profile snapshot cleared; cost and status survive as anonymous accounting data, the same treatment as AI usage records above - purchase records — retained with the email hashed, per the 7-year row above
- parental consent email records — retained with the recipient address hashed, same treatment as purchases. The record that a notice was sent (or bounced) survives; the parent's actual address doesn't.
Scholarships discovered during a user's search stay in the shared corpus. They are facts about third-party scholarship programmes, not personal data, and they carry no link back to the user.
Aggregate counts
Some numbers we keep permanently, on purpose — total signups, total searches run, and similar all-time totals we use for our own records. These are running counts, not copies of anyone's data: they hold a number, nothing else, and were never traceable back to an individual in the first place. Deleting an account does not decrease them, the same way it doesn't remove that account's share of tax or chargeback history from a purchase total — the count reflects that something happened, not who it happened to. A count broken down finely enough that a single person could be inferred from it would be personal data after all; ours are kept coarse (totals, at most a daily figure) specifically so that line is never crossed.
Backups
Backups are retained 35 days. A deletion request is applied to live data immediately; backups age out on their own. We do not restore deleted personal data from a backup, and if a backup is ever restored for another reason, deletions in the intervening period are re-applied before it returns to service.
This isn't theoretical: a routine restore test decrypted a real backup and found an account that had, by then, already been erased from the live database. That's correct behaviour — a snapshot can't be edited after the fact — and it's the concrete version of what "35-day window" means in practice: for up to 35 days, an erased account's data still exists in at least one encrypted backup, unreachable without a key kept off the server, until that backup ages out on its own.
Backups are encrypted, with the decryption key kept off the server entirely. That materially changes what the 35-day window means: someone who reaches the server gets ciphertext, not 35 days of data people asked us to destroy. Without this, an unencrypted backup set would have been the largest gap in this policy — a full archive of erased data, sitting in the place most likely to be compromised.
Review
Annually, or whenever a new data type is added. A new column that holds personal data doesn't ship until it has a row in this table.