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). 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
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_atis set, which locks the account (every household procedure answersFORBIDDEN; 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.previewlists 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 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:
pendingErasureserases due accounts (up to 5 per tick) beforependingStoragePurgeruns.
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
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
| 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
pendingDataExports(data-export.jobs.ts): deletes up to 25 ZIPs past their window (stateexpired), 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 markedfailed; a requester who is no longer a Member, or no longer an owner or admin, getsfailed. The export holds every car, so a Member granted only some of them (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 auditsuser.deletedin 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 oforgs/,.trash/orgs/,derived/orgs/,incoming/orgs/andexports/orgs/for it; the row goes once a round finds nothing.
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
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
packages/api/src/features/data-export/data-export.jobs.tspackages/api/src/features/data-export/data-export.archive.tspackages/db/src/erase-user.tspackages/api/src/features/erasure/erasure.service.tspackages/api/src/features/erasure/erasure.jobs.tsapps/mobile/src/features/data-export/data-export.screen.tsx