# Data export and erasure

How a Member exports the household's data as CSV and PDF, and what deleting an account erases, when.

## What it does [#what-it-does]

**Export my data** (US29, RF22) on M-Perfil queues a Data Export. The cron job builds a ZIP under `exports/orgs/<org>/<id>.zip`: a README, one CSV per table (vehicles, ownership periods, ledger entries, expenses, services, odometer readings, loans and their balances, valuations, acquisitions, disposals, reminders, recurring expenses, documents and photos metadata, the requester's preferences and consents) and a PDF summary per car in `summaries/`. CSVs are UTF-8 with a BOM, amounts in minor units with their ISO 4217 code, distances in km; a cell that starts with `=`, `+`, `-` or `@` is prefixed with `'` so a spreadsheet never runs it. The PDF (a dependency-free text PDF, `data-export.pdf.ts`) is in the requester's language: purchase price, total spent, interest and fees, services, the latest estimated value with source · date · confidence (RN03) and, for a sold car, the deal result and the economic result apart (RF20). It never says "profit" (RN04).

Plate and VIN are decrypted into the export only when the requester is the Organization's owner (RN09); nobody else's export has them. The document and photo files themselves are not copied.

The ZIP stays 7 days (`DATA_EXPORT_KEEP_DAYS`); `dataExport.latest` signs a 15-minute link while it is ready. When the requester asked for email reminders and has a verified email, the job emails them once that it is ready (no link in the email: the app signs one).

**Delete account and data** erases (packages/db `erase-user.ts`), from Better Auth's `user.delete.before` hook (self-service, "Delete now" and admin removal alike) and from the erasure job when a scheduled deletion is due: every household nobody else manages goes with the User, as long as they manage it or are its only Member (`householdsErasedWith`). Deleting the Organization cascades to every AutoFolio row, ★ append-only ones included. That is the one sanctioned exception to append-only, run as system code with the `account.erase` SystemReason; the `append_only` trigger lets a row go only together with its owner ([ADR-0019](/docs/adr/0019-erasure-after-a-grace-period)). The same transaction queues the Organization in `storage_purge` and moves another Member's session that was acting in it to their next household. A household another owner or admin manages stays; the User only leaves it. The User gets an email that the account is gone, in their language, and each other Member who used one of its cars gets one saying what became of it (deleted with the account, or the history they accepted stays in their garage; `erasureNotices`, read before the erasure).

## Deletion with a grace period [#deletion-with-a-grace-period]

With the flag `account.deletion-grace` (privacy spec §2), Delete account schedules the erasure 30 days ahead instead of running it:

- **At once** (`erasure.schedule`): `user.deletion_scheduled_at` is set, which locks the account (every household procedure answers `FORBIDDEN`; the app shows only P-Excluir-Agendada); every session and MCP token of the User goes; passports, showcases, unused invite links and pending invitations of the households that go with them, or made by them, are revoked. The User is emailed the date; each Member who uses one of those cars is emailed what happens to it.
- **Cars other people use**: `erasure.preview` lists the cars of those households granted to other Members. Each is either deleted with the account or handed to one Member who uses it as a [History Transfer](/docs/features/history-transfer) with all of its technical history (services, readings, photos, documents; never money, plate or VIN). The Member accepts it into their own garage before the date; the copy's rows are theirs, marked "from previous owner", so nothing in it points at the deleted User. An offer not accepted goes with the household.
- **Keep my account** (`erasure.cancel`): the lock goes, the handovers not yet accepted are withdrawn; revoked links stay revoked.
- **Delete now**: Better Auth's `deleteUser`, the immediate path above, Members' notices included.
- **After 30 days**: `pendingErasures` erases due accounts (up to 5 per tick) before `pendingStoragePurge` runs.

**Web** mirrors the app: Settings → Security → Danger zone opens P-Excluir as a dialog (`screen-P-Excluir`, the same choices and `excluir-escolha-<vehicleId>`, Schedule deletion `excluir-agendar`; there is no web export, so no Export first) and lands on `/login` signed out. The `/_auth` guard sends a scheduled account to `/deletion-scheduled` (`screen-P-Excluir-Agendada`: Keep my account, Delete now after a confirm dialog, Sign out), outside the app shell, so no household request is made. Without the flag the Danger zone keeps the type-your-email immediate deletion. The choice rules (`defaultChoice`, `transfersOf`, `daysLeft`) live in `erasure.contract.ts` for both.

Audit actions: `account.deletion_scheduled`, `account.deletion_cancelled`, `history_transfer.offered` per handover, `user.deleted`.

## Timeline [#timeline]

`ERASURE_TIMELINE` (`erasure.contract.ts`) is the one source for the app and the privacy policy: the grace period (`graceDays`, 30), sign-out and the database rows when the erasure runs, files erased within 30 days (in practice the next cron minutes), encrypted backups expire after 35 days (RNF09). Change the numbers there; the copy interpolates them.

## Procedures [#procedures]

| Procedure            | Who                         | Failures                                                                       |
| -------------------- | --------------------------- | ------------------------------------------------------------------------------ |
| `dataExport.latest`  | Members of the Organization | —                                                                              |
| `dataExport.request` | Owners and admins           | —                                                                              |
| `erasure.preview`    | The signed-in User          | —                                                                              |
| `erasure.schedule`   | The signed-in User          | `CONFLICT` already scheduled, `BAD_REQUEST` a car or Member not in the preview |
| `erasure.cancel`     | The signed-in User          | —                                                                              |

`request` is audited (`data.export_requested`). While one export is queued, a second request returns it (a unique partial index).

## Jobs [#jobs]

- `pendingDataExports` (`data-export.jobs.ts`): deletes up to 25 ZIPs past their window (state `expired`), then leases the oldest queued export for 5 minutes and builds it. A build that crashes is retried on a later tick, at most 3 times, then marked `failed`; a requester who is no longer a Member, or no longer an owner or admin, gets `failed`. The export holds every car, so a Member granted only some of them ([Family garage](/docs/features/family-garage)) asks for none.
- `pendingErasures` (`erasure.jobs.ts`): locks one due User at a time (`for update skip locked`), erases their households, deletes the User and audits `user.deleted` in one transaction, then emails them.
- `pendingStoragePurge` (`erasure.jobs.ts`): leases one erased Organization and hard-deletes (bypassing the trash) up to 100 objects under each of `orgs/`, `.trash/orgs/`, `derived/orgs/`, `incoming/orgs/` and `exports/orgs/` for it; the row goes once a round finds nothing.

## Data and isolation [#data-and-isolation]

`data_export` has row-level security: a Member reads and inserts only their own rows in the Active Organization, and only as `pending` without a key; the job alone marks them ready. `storage_purge` is system only (no policy, no grant). `user.deletion_scheduled_at` is not granted to tenant traffic; the erasure procedures write it on the system handle.

## App [#app]

M-Perfil → Export my data opens `/export-data`: what the ZIP holds, then the latest export with one action (Prepare export, Download ZIP or Try again). The screen polls every 10 seconds while the export is pending. Delete account (`/delete-account`) is P-Excluir with `account.deletion-grace` (`screen-P-Excluir`): what happens at once and in 30 days, each car other people use with a menu (`excluir-escolha-<vehicleId>`: transfer to a Member who uses it, or delete with the account; the one Member who logs on a car is the default), Export my data first and Schedule deletion (`excluir-agendar`) after a confirm sheet. Without the flag it is the immediate deletion: what is deleted and when, the word DELETE (EXCLUIR, LÖSCHEN) and a confirm sheet. A signed-in account with a scheduled deletion only reaches `/deletion-scheduled` (P-Excluir-Agendada, `screen-P-Excluir-Agendada`): the date and days left, Keep my account (`agendada-manter`), Delete now (`agendada-excluir-agora`, after a confirm sheet) and Sign out. Dev links: `autofolio://dev/P-Excluir`, `autofolio://dev/P-Excluir-Agendada` (a preview 27 days ahead; the seed account is not locked). The Face ID lock is the template's app lock: an opaque cover over the mounted app, so push registration and reminders are never blocked.

## Key files [#key-files]

- [`packages/api/src/features/data-export/data-export.jobs.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/data-export/data-export.jobs.ts)
- [`packages/api/src/features/data-export/data-export.archive.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/data-export/data-export.archive.ts)
- [`packages/db/src/erase-user.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/db/src/erase-user.ts)
- [`packages/api/src/features/erasure/erasure.service.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/erasure/erasure.service.ts)
- [`packages/api/src/features/erasure/erasure.jobs.ts`](https://github.com/CarlosZiegler/autofolio/blob/main/packages/api/src/features/erasure/erasure.jobs.ts)
- [`apps/mobile/src/features/data-export/data-export.screen.tsx`](https://github.com/CarlosZiegler/autofolio/blob/main/apps/mobile/src/features/data-export/data-export.screen.tsx)

