# Compliance: export, deletion, retention, privacy (M6)

## Data export (portability)

Settings → Your data → *Prepare export* (owner only, password confirmed within 15 minutes) queues `ExportTenantData`. It produces a ZIP on `TENANT_EXPORT_DISK`:

- `tenant/{table}.json` for every tenant table. Tables in `compliance.excluded_tables` (OAuth tokens, outbox, webhook receipts, migrations) are left out, and columns named like tokens, secrets or passwords are removed. Message bodies are decrypted.
- `workspace.json`: tenant, domains, members (name, e-mail, role), subscriptions, billing profile, invoices.
- `media.csv`: manifest of media files (paths and SHA-256 checksums). The files themselves stay in storage.

The download link checks the owner role and a fresh password, and the download is audited. Exports expire after `TENANT_EXPORT_RETENTION_DAYS` (`exports:prune`).

## Workspace deletion

1. The owner types the workspace slug (with a fresh password). This sets `deletion_scheduled_for` = now + `TENANT_DELETION_GRACE_DAYS` (30), e-mails the owner and shows a banner. It can be cancelled until then.
2. `tenants:purge-due` (daily):
   - sets the status to `deleting`;
   - writes a final export (`reason = pre_deletion`, kept `TENANT_FINAL_EXPORT_RETENTION_DAYS`). **If the export fails, nothing is dropped**, and the purge is retried on the next run;
   - drops the tenant database and its MySQL user (provisioner connection) and deletes the stored DB secret;
   - removes webhook routes and identity links, revokes memberships, and marks the tenant `deleted` (soft-deleted). Invoices and payments are kept, as tax law requires.

## Retention jobs

| Command | Removes |
|---|---|
| `metrics:prune` | metric values > 760 days, sync runs > 90 days |
| `reports:prune` | report files > 90 days |
| `inbox:prune` | closed conversations > 365 days, webhook receipts > 30 days |
| `exports:prune` | expired data exports |
| `oauth:prune-states` | expired OAuth states |

## Privacy notice (DPDPA 2023)

- Public notice at `/privacy` (central). Acceptance is required at signup and invitation acceptance, and is recorded on the user (`privacy_notice_version`, `privacy_notice_accepted_at`).
- Raising `PRIVACY_NOTICE_VERSION` sends every signed-in user to an acceptance page on their next request (middleware `privacy`). Acceptance is audited.
- Grievance contact: `PRIVACY_GRIEVANCE_OFFICER`. Review the notice text with counsel before launch (docs/plan/11).
