← Ataski · Trust & security · DPA · Subprocessors

This is the canonical English version. Native translations for DE / ES / FR / PT / RU are pending counsel sign-off.

Approved 2026-09-05

Data Retention Policy (internal)

Owner: the founder. Approved: 2026-09-05. Review: annually.

The published retention terms live in docs/legal/data-retention-policy.md and are served at /legal/data-retention. They are a customer commitment. This page is the internal layer: which mechanism discharges which promise, and where the seams are.

1. The rule that governs everything else

A published retention term must be enforced by a mechanism, not asserted by a sentence. Publishing a term nobody honours is a storage-limitation failure under GDPR Art. 5(1)(e) and a transparency failure under Art. 13(2)(a) — and it is the first line an auditor or a customer's security questionnaire writes down.

Where the number and the mechanism could drift apart, they are bound in code: AUDIT_LOG_RETENTION_MONTHS is one integer that the SQL interpolates and the published policy quotes, and tests/test_audit_log_retention.py fails if the two stop being the same number.

2. Headline terms

Data class Term Discharged by
Customer Data (CRM, outreach, drafts, contracts) subscription life + 30 days src/ataski/offboarding.py (three-phase purge)
Contact PII, per tenant same as the parent customer same purge
Firmographic cache (non-PII, cross-tenant) 30 days from last confirmation src/ataski/firmographic_retention.py
Outreach send history 24 months src/ataski/outreach_retention.py
Audit log 24 months, then scrub src/ataski/audit_retention.py
Global opt-out register (hashed) indefinite — legal obligation never auto-purged
Right-to-delete request log at least 7 years retained as proof of erasure

3. Deletion is a scrub where the ledger must survive

The audit log is append-only at the database grant level: the runtime role holds SELECT and INSERT, and REVOKE UPDATE, DELETE is explicit. So the 24-month term is discharged by nulling the personal columns and emptying the payload while keeping the action / actor / resource / cost skeleton (ADR-0107). A retention term is satisfied by ceasing to hold the personal data, not by destroying the record; deleting rows would also punch holes an auditor cannot tell apart from tampering, and would take the cost history the spend rollups depend on with it.

4. Workspace deletion

Scheduled with a 30-day undo → a committed write fence that refuses every object-storage write and presigned URL for that tenant → a hard delete of every tenant-scoped row, then the R2 objects, then a data_purged_at stamp as proof. A tenant with an unsettled write intent is deferred to the next pass rather than purged under an in-flight write. A verifier re-checks the result.

What survives, and why, is enumerated in the published policy: the workspace tombstone (accountability, Art. 5(2)), deletion-request records with the requester's e-mail hashed, billing records for 7 years (Art. 17(3)(b), with names, e-mails and addresses dropped before storage), and PII-free cost ledgers.

5. Two registries keep this honest

Both are validated against the live schema in the database stage of the pre-push gate (scripts/check.py). That is what stops the three ideas of "what tenant data exists" — the schema's, the purge's and the export's — from drifting apart, which the 2026-07-14 audit found to be the biggest systemic theme.

6. Customer rights

Export at /api/export — the path the DPA, the Terms and the privacy policy all promise — which redirects to the per-tenant JSON archive. Individual deletion for outreach recipients at /privacy/optout, within 30 days (GDPR) or 45 days (CCPA). Vendor cache propagation is part of the individual-deletion path, not an afterthought.

Waitlist and contact-sales addresses (waitlist_signups, enterprise_inquiries) belong to no workspace, so no purge reaches them, no retention job ages them out, and the runtime role cannot delete them (0153, 0601). Ataski is the controller here, so the request comes in under Privacy Policy §13.3 — usually an email to security@ataski.com. To honour it:

  1. Verify the requester controls the address: the request came from that mailbox, or they answered a confirmation sent to it. Never erase on an unverified request.
  2. Open /admin/privacy/erasure, enter the address, press Erase. It deletes every row for the address in both tables (case-insensitive, after the same normalisation the forms apply) and, in the same transaction, writes one gdpr.public_form_subject_erased audit row under PLATFORM_TENANT_ID that holds only the address hash. Without PLATFORM_TENANT_ID it refuses and deletes nothing. A repeat is a recorded no-op. The opt-out register is not touched.
  3. Delete the contact-sales notification email for that person from the founder mailbox, then confirm to the requester — within 30 days (GDPR) or 45 days (CCPA) of the request.

The same page lists /privacy/optout submissions for an address that is still on one of these forms. That form proves nothing about who typed the address, so it does not delete by itself: verify as in step 1, then erase from the list.

7. Open items

Backups are the seam. A restored snapshot can reintroduce rows a purge removed, and nothing today documents how a deletion request is re-applied after a restore. That belongs in the restore drill (see business-continuity.md) and is the one retention question this policy cannot yet answer.