Skip to content

feat: delete your account (GDPR) (#134) - #167

Merged
clbotdev merged 5 commits into
mainfrom
feat/134-account-delete
Oct 29, 2026
Merged

clbotdev merged 5 commits into
mainfrom
feat/134-account-delete

Conversation

@clbotdev

Copy link
Copy Markdown
Contributor

Closes #134. Final sub-issue of #152.

What

  • API GET /account/deletion (preview), POST /account/deletion/code (emails a code), POST /account/deletion (typed email + code). Dashboard session only, same origin/CSRF checks as billing actions; REST-only (MCP parity exemption).
  • Organizations where you're the last owner → deleteOrganization() (keys, domains, ESPs, MediaLit files) + eraseOrganizationPersonalData() (contacts and cascades, signup confirmations, transactional recipients/bodies, recipient addresses via the now-used anonymize…ForPrivacyDeletion helpers, raw webhook payloads, reputation connections). Suppression hashes stay.
  • Shared organizations: only the membership goes (removeOrganizationMember, audit organization.member_account_deleted).
  • Blockers (nothing changes): running subscription / open checkout on a sole-owned org (billing never cancelled automatically), last owner with other members, paying for a shared org.
  • Identity: sessions, accounts, OAuth tokens/consents/clients, OTP rows deleted; user row becomes a tombstone (deleted_at, migration 0029, Better Auth additional field) refused by resolve-auth/ensureDefaultOrganization. Re-signup with the same email = fresh empty account. Abuse flags drop email/IP.
  • account_deleted event with user id + counts only (allowlist updated).
  • Web Account → Danger zone → dialog following the domain-auth dialog pattern (per-org sections, blockers open, progress summary, "Confirm it's you" step, footer actions). PostHog browser identity reset after delete.
  • Docs account.mdx, errors.mdx, mcp.mdx; ADR 0009 for erasure semantics (why tombstone vs Better Auth deleteUser, what's kept for billing/trial/audit, no later purge job).

Checks

  • API suite 945+10 pass, web 108+2 pass, lint, tsc, check:drift, test:conformance.
  • Browser e2e (isolated stack, MediaLit stubbed, Mailpit real SMTP): see comment below.

…ntacts' data (#134)

GET /account/deletion previews what happens to each organization and what
blocks the delete; POST /account/deletion/code emails a confirmation code;
DELETE /account takes the typed email and code. Dashboard sessions only,
with the billing actions' origin and CSRF checks.

Organizations the person is the last owner of are deleted through
deleteOrganization (MediaLit files included) and their contacts' personal
data erased; suppression hashes stay. Shared organizations are only left.
Sessions, sign-in methods, OAuth tokens, consents and codes are deleted and
the user row becomes an anonymous tombstone (user.deleted_at), which auth
refuses. Blocked by a running subscription, an open checkout, other members
depending on them as last owner, or paying for a shared organization.
…board proxy forwards the body (#134)

The web proxy drops DELETE bodies, so the typed email and code never
reached the API. Also rate-limit deletion codes per person rather than per
IP, which the proxy shares.
@clbotdev

Copy link
Copy Markdown
Contributor Author

Browser e2e (Playwright, isolated stack on 5180/3180, fresh DB, MediaLit stubbed, real SMTP to Mailpit)

Two users: the owner has a sole-owned org (team, 2 contacts, a broadcast with an uploaded image sent through Mailpit) and is co-owner of the partner's org.

ok - partner org org_URFVwVqmmNBT5zkD3NnijImp
ok - owner org org_LnUjOu8YPk2w7gvnzi0ksRFA with team
ok - owner added to partner org (201)
ok - mailing address
ok - contact 1
ok - contact 2
ok - broadcast sent
ok - 2 outbound messages with recipient addresses (2)
ok - media row for lit0005mvtjck0d
ok - partner contact
ok - blocker: add another owner first
ok - progress 1 of 2
ok - send code disabled while blocked
ok - delete disabled while blocked
ok - API preview blocked
ok - partner removed from owner org (204)
ok - progress 2 of 2
ok - sole org: will be deleted
ok - shared org: you'll leave
ok - deletion code emailed (dev log)
ok - wrong code rejected
ok - signed out to http://localhost:5180/login?redirect=http%3A%2F%2Flocalhost%3A3180%2F
ok - identity erased, tombstone
ok - email gone
ok - session gone
ok - account gone
ok - organization_members gone
ok - team_members gone
ok - sole-owned org closed
ok - contacts erased
ok - outbound recipients erased
ok - outbound rows kept for reporting
ok - media rows deleted
medialit deletes: [ 'lit0005mvtjck0d' ]
ok - MediaLit delete for the sent image
ok - partner org active
ok - partner contacts kept
ok - audit event in shared org
ok - no email in account_deleted log
ok - old session cookie -> 401
ok - partner org has only the partner now
ok - new user id
ok - one new organization
ok - no contacts
ALL OK

Screenshots checked: danger zone, blocked dialog ("Add another owner first", Send code and Delete disabled), ready dialog with the sole-owned org expanded, wrong-code error, signed-out login page, empty fresh account. The first e2e run found that the web proxy drops DELETE bodies; fixed in 60f451b by moving the confirm step to POST /account/deletion.

@clbotdev
clbotdev merged commit dbd3332 into main Oct 29, 2026
5 checks passed
@clbotdev
clbotdev deleted the feat/134-account-delete branch October 29, 2026 12:47
@clbotdev clbotdev mentioned this pull request Oct 29, 2026
8 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delete account

2 participants