feat(account): let users delete their own account - #6831
Conversation
Adds a GDPR self-serve account deletion path: a preflight that reports what deletion would remove and every reason it would be refused, and a confirmed delete that erases the account and everything only it can reach. Deletion refuses while the account is still entangled rather than reassigning its content. Most tables reference user.id with ON DELETE CASCADE, and those cascades do not distinguish content in the account's own workspace from content it created inside somebody else's, so each blocker names the existing action that untangles it (leave the workspace, leave the organization, cancel the plan) — all of which already hand work over on their own tested paths.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview The server preflights a deletion plan (blockers, workspaces to delete vs transfer) and refuses while the account is still entangled—paid org ownership, org membership, active personal plan, shared/org workspaces, or owned data drains—instead of relying on broad Better Auth Reviewed by Cursor Bugbot for commit 1f06819. Configure here. |
Greptile SummaryThe PR adds session-only self-service account deletion with blocker previews, transactional workspace teardown and reassignment, post-commit object cleanup, audit recording, and a confirmation UI.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; the previously reported partial-deletion, stale-membership, and committed-transfer issues are addressed by the current transactional ordering and guarded deletion.
|
| Filename | Overview |
|---|---|
| apps/sim/lib/users/account-deletion.ts | Implements deletion planning, guarded transactional teardown, workspace-anchor reassignment, and post-commit object cleanup; the previously reported transaction and stale-membership failures are addressed. |
| apps/sim/lib/workspaces/utils.ts | Adds transaction-executor support to billing and ownership reassignment helpers so their writes participate in account-deletion rollback. |
| apps/sim/lib/users/application/delete-account.ts | Enforces session-only deletion and email confirmation while coordinating deletion and actor-less auditing. |
| apps/sim/app/api/users/me/deletion/route.ts | Exposes session-authenticated preview and deletion endpoints through typed route contracts. |
| apps/sim/app/workspace/[workspaceId]/settings/components/general/components/delete-account-modal.tsx | Adds the blocker-aware account-deletion confirmation dialog and post-deletion client cleanup. |
| apps/sim/lib/users/account-deletion.test.ts | Covers blocker classification, workspace categorization, error mapping, and profile-picture key extraction. |
Sequence Diagram
sequenceDiagram
participant U as User
participant UI as Settings UI
participant API as Account deletion API
participant DB as Postgres
participant S as Object storage
participant A as Audit
U->>UI: Open deletion dialog
UI->>API: GET /api/users/me/deletion
API->>DB: Load blockers and workspace plan
DB-->>API: Deletion facts
API-->>UI: Plan or blockers
U->>UI: Confirm account email
UI->>API: POST /api/users/me/deletion
API->>DB: Collect storage keys
API->>DB: Begin transaction
API->>DB: Guarded workspace deletes
API->>DB: Reassign billing and ownership
API->>DB: Delete user
DB-->>API: Commit
API->>S: Purge collected objects
API->>A: Record actor-less account.deleted event
API-->>UI: Success
UI->>U: Clear local state and redirect to login
Reviews (3): Last reviewed commit: "fix(account): close deletion gaps found ..." | Re-trigger Greptile
Reorders the teardown so nothing irreversible happens before the deletion is certain: anchors are handed over first (the fallible step, while everything is still recoverable), the workspace and user deletes now share one transaction, and the object-storage purge runs only after that commits. The workspace delete also re-checks inside the transaction that each workspace is still private, so a membership granted between the preview and the delete aborts the whole thing instead of destroying the new member's access.
|
@cursor review |
- Run the whole teardown in one transaction. The billing and ownership handovers now take the caller's transaction, so a refused deletion can no longer leave a workspace reassigned for a deletion that never happened. - Fail closed on a subscription read error. getHighestPriorityPersonalSubscription defaulted to returning null, which read as "no plan" and would have erased an account Stripe was still billing. - Erase the account's profile picture. It is personal data under our own storage prefix; an external provider avatar is left alone. - Enforce the storage purge cap while collecting keys rather than after, so an oversized account cannot exhaust memory before the cap applies.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 1f06819. Configure here.
Summary
GET /api/users/me/deletionpreviews what deletion removes;POSTperforms it. Both go through an application use case, session-only — an API key or delegated service can never erase the account behind it.user.idwithON DELETE CASCADE, and those cascades don't distinguish content in your own workspace from content you created inside someone else's — so each blocker names the existing action that untangles it (leave the workspace, leave the organization, cancel the plan), all of which already hand work over on their own tested paths.workspace.billed_account_user_idisNO ACTIONand Postgres evaluates it before theowner_idcascade that would remove the same workspace, so anchored workspaces are torn down or handed over before theuserrow is touched.deleteUserstays disabled and itsbeforeDeletenow refuses unconditionally, so there is exactly one deletion path and flipping the flag can't route around the preflight, purge, teardown or audit.account.deletedaudit entry with no actor identity — a compliance record that doesn't retain the person who just exercised their right to erasure.disabledTooltiptoChipConfirmActionso a blocked confirmation can state its own remedy.Type of Change
Testing
Tested manually. 16 unit tests covering the classifier (every blocker, both workspace buckets, name formatting) and the use case (email confirmation, non-session principals); verified the suite fails when the classifier is broken.
turbo run type-check,bun run lint,bun run check:audits(29/29) and the emcn chip-modal suite all pass.Checklist