feat(web): delete an account by e-mail link, and make the Danger Zone actions work
Problem
- Ever Teams had no way to delete an account without signing in. People who lost access, or whose email has accounts in several workspaces, had no path, and store/privacy rules require one. ever.team/delete also described an in-product flow that did not fully work.
- In Settings → Personal → Danger Zone, "Delete all data" called no API at all: its handler was
console.log(user). Every failed action also left its confirm modal open without a word (an unhandled promise rejection).
What changed
Public email-link deletion (contract shared with ever-co/ever-gauzy#11519: POST /api/account-deletion/{request,verify,confirm})
/[locale]/delete-account: an email form. It sendsemail,callbackUrl = <origin>[/<locale>]/delete-account/confirm,localeand the app's email branding (appName,appLogo,appSignature,appLink,companyName,companyLink). These are the same fields the Ever ID sign-in sends, now shared throughcore/lib/helpers/app-email-branding.ts. The page always shows the same answer: "If an account exists for that email, we sent a link to delete it. The link expires in 30 minutes."/[locale]/delete-account/confirm#token=…:- The token is read from the fragment and removed from the address at once, then
verifyruns. - The page lists every workspace and its outcome:
workspace-deletedmeans the person is the only member;account-removedmeans the account and member records are deleted and shared content stays without the name. A warning shows whensoleAdminis true andotherMembers > 0. The page also says what is kept. - The person ticks a checkbox and types
DELETE, thenconfirmruns with the preview'splanHash. - A 409 (the accounts changed since the preview) shows the new preview with a notice and asks for the confirmation again.
- A 410 is the only "invalid or expired" answer. Network errors, 5xx and 429 show a transient error with Try again, which re-runs the call with the token kept in memory.
- After deletion, a local session whose account is gone is cleared (cookies and query cache).
- A link pasted into the same tab, which only changes the fragment, is also read.
- The token is read from the fragment and removed from the address at once, then
- Proxy mode (no
NEXT_PUBLIC_GAUZY_API_SERVER_URL):app/api/account-deletion/{request,verify,confirm}forward to Gauzy and keep its status.app/api/account-deletion/me(GET/POST) forwards the caller's token and tenant. - Entry points: a Delete my account by email link in the sign-in footer (passcode and password pages), and a Delete by email link row in the Danger Zone.
Danger Zone
- Delete all data now deletes the signed-in account with the same rules:
POST /api/account-deletion/me {confirm:true, planHash}, permissionACCESS_DELETE_ACCOUNT.- Its modal first shows
GET /api/account-deletion/me: the outcome in this workspace, other members, the sole-admin warning, what is kept, and that the user will be signed out. Confirm stays disabled until that preview has loaded. - After deletion the user is signed out: cookies and query cache are cleared and the app goes to
/auth/passcode. - It deliberately does not use
DELETE /user/reset. Gauzy's factory reset wipes every member's data in the user's organizations and keeps the account.useUser().resetUseris kept, but nothing calls it now.
- Its modal first shows
- Delete this account (
DELETE /api/user/:id) used to invalidate the user queries before logging out. That refetched/user/mewith the deleted account's token, which answered 401. The queries are now removed instead. RemoveModalshows the reason when an action fails and resets its message when it closes. This also fixes the team Danger Zone, which uses the same modal.
i18n: pages.accountDeletion.* (36 keys) and alerts.ACTION_FAILED in all 13 locales.
Danger Zone audit
| Button | API and payload | Before | After (verified on the local API) |
|---|---|---|---|
| Remove from all teams | DELETE /api/organization-team/teams/:userId, no body, bearer + tenant-id | Success: teams, token and user reload. Failure: a toast, plus an unhandled rejection that left the modal open | Same success path. Failure: the API's reason shows in the modal ("You are not able to removed account where you are only the manager!") |
| Delete this account | DELETE /api/user/:id, no body (Gauzy ALL_ORG_EDIT / ACCESS_DELETE_ACCOUNT) | Success: invalidate (a 401 refetch), then logOut(). Failure: nothing shown, unhandled rejection, modal stuck open | 200, then signed out, auth cookies cleared, /auth/passcode. Failure: the reason shows in the modal |
| Delete all data | Before: no request. After: GET + POST /api/account-deletion/me {confirm:true, planHash} | Did nothing (console.log(user)), and the modal never closed | Preview in the modal → 200 → signed out. Signing in again gets no code email, and a guessed code is refused |
| Delete by email link (new) | Link to /delete-account | did not exist | The public email-link flow, for every account with that email |
Observed on the API side, not worked around here: Gauzy declares DELETE /api/user/:id (UUID pipe) before DELETE /api/user/reset. The client no longer calls /user/reset.
Security notes
- The token travels only in the URL fragment, so it never reaches a server log or a
Referer.- The confirm page reads it in a layout effect, before any passive effect such as analytics page views, and immediately calls
history.replaceState(null, '', pathname). Thenullstate makes Next.js sync its router to the clean URL rather than write the old one back. The token then lives only in a React ref.
- The confirm page reads it in a layout effect, before any passive effect such as analytics page views, and immediately calls
<meta name="referrer" content="no-referrer">comes from the route layout's metadata. next.config.js also sendsReferrer-Policy: no-referrerandX-Robots-Tag: noindex, nofollowfor/delete-account/**and/<locale>/delete-account/**.- Third-party scripts: the Sentry browser SDK, PostHog and Jitsu stay off on any page load that starts on these routes (
isAccountDeletionPath), because they record the URL they start on. The existing PostHog/Jitsu redaction also strips atokenfragment. GA is commented out, and Chatwoot only loads on the home page. - Both routes are public: they are not in
PROTECTED_APP_URL_PATHSand not under/auth/*, so a signed-in visitor is not redirected away. No session is created. - The request form always gives the same answer. The public calls send no
Authorizationheader and no tenant or organization ids, even when a session is open in the same browser. - Proxy routes forward only the expected string fields and never log bodies.
Screenshots
All screenshots come from real local runs: the Ever Teams dev server in direct mode, against the local Gauzy API from ever-co/ever-gauzy#11519 (with planHash), driven by headless Chromium. 23 and 24 stub the API answers in the browser to show the states the API cannot produce on demand. The files are under docs/pr-assets/account-deletion/.
Danger Zone (signed in)
| Step | Screenshot |
|---|---|
| 01 Avatar menu → Settings | |
| 03 Danger Zone with the new "Delete by email link" row | ![]() |
| 05 Remove from all teams: the API's refusal now shows in the modal | ![]() |
06 Delete this account (DELETE /api/user/:id) | ![]() |
| 07 …then signed out, auth cookies cleared | ![]() |
08 Delete all data: preview from GET /api/account-deletion/me | ![]() |
09 …POST /api/account-deletion/me 200, then signed out | ![]() |
| 10 Signing in again: no code email, and a guessed code is refused | ![]() |
Email link (no sign-in)
| Step | Screenshot |
|---|---|
| 11 Sign-in footer link | ![]() |
12 /delete-account | ![]() |
| 13 The same answer whether or not the account exists | ![]() |
| 14 The email (the token is in the link's fragment) | ![]() |
| 15 Confirm page: token gone from the address bar; sole member, so the workspace is deleted | ![]() |
| 16 Checkbox + typed DELETE | ![]() |
| 17 Deleted | ![]() |
| 19 The same link again: 410 | ![]() |
| 21 Shared workspace: account removed, sole-admin warning | ![]() |
| 23 (stubbed API) 409: the new preview, confirmation cleared | ![]() |
| 24 (stubbed API) 503: Try again re-uses the in-memory token | ![]() |
docs/pr-assets/account-deletion/ also has 02 (Settings), 04 (the remove modal), 18 (the completion email), 20 (sign-in refused) and 22 (the shared-workspace result).
Test plan
Automated (run locally on this branch):
- jest:
core/lib/auth/account-deletion.test.ts: paths, confirm URL, the token is taken from the fragment or query and removed withreplaceState(null), the sole-admin rulecore/services/server/requests/account-deletion{,-me}.test.ts: the proxy routes forward only the expected fields andplanHash, pass 202/410/409/403 through, answer 400 for a missing field and 502 when Gauzy is downtest/sentry/sentry-client-instrumentation.test.ts: Sentry stays off on/delete-account,/delete-account/confirmand/fr/delete-account/confirmtest/architecture/feature-surface-preserved.test.ts: route surface of 141 files and 188 verbs- the Ever ID sign-in tests, which use the shared branding helper
-
tsc --noEmiton apps/web: 0 errors.eslinton the changed files: clean
End to end against the local Gauzy API (ever-co/ever-gauzy#11519):
- Danger Zone "Delete all data" in a sole-member workspace: the preview says workspace-deleted.
POST /mewithplanHash→ 200, then signed out (auth cookies cleared,/auth/passcode). Signing in again gets no code email, and a guessed code → 401 - Danger Zone "Delete this account":
DELETE /user/:id→ 200, then signed out - Danger Zone "Remove from all teams" as the sole manager: the API's refusal shows in the modal
- Email link, sole member: request 202 → email with
/delete-account/confirm#token=…→ the token leaves the address bar on load → verify → confirm (bodytoken, confirm, planHash) → 200{accounts:1, workspacesDeleted:1}→ the same link again → 410 → sign-in refused - Email link, sole admin of a workspace with another member: account-removed with the sole-admin warning →
{accounts:1, workspacesDeleted:0} - Confirm page:
Referrer-Policy: no-referrerheader and meta, andX-Robots-Tag- no third-party script, and no request URL or Referer carries the token
- Sentry, PostHog and Jitsu are not started (also checked in the built-in browser)
- The 409 (changed preview) and 503 (transient) states, with the API stubbed in the browser
Not done yet:
- Proxy mode (
NEXT_PUBLIC_GAUZY_API_SERVER_URLunset): unit-tested, not run end to end - Locales other than English: the strings are in all 13 locales, but only the English pages were looked at
-
yarn build:webwas not run (fast-build rule)
Notes
- Contract changes adopted:
GET|POST /api/account-deletion/mefor the Danger Zone (the lead's decision)planHashon verify,/me, confirm andPOST /me, with 409 handling- only 410 means a dead link; 429, 5xx and network errors are transient
- Completion email branding: lane A fixed it in ever-gauzy 5b49897e86. The "deleted" email now uses the branding stored with the link's request, and
POST /meaccepts the same optional branding fields. Ever Teams sendsappEmailBranding()withPOST /me(commit 02108018c); the screenshots predate that fix. The 500 ofPOST /mewas fixed in ever-gauzy 5406e92b68. - Seen but not changed (pre-existing):
- the
auth-active-projectandauth-timezonecookies outlive a logout - the browser tab title of
/delete-accountreads "Home" (formatTitledrops hyphenated segments) - the sidebar's resize rail draws over modals
- the
Depends on ever-co/ever-gauzy#11519 (lane A: the account deletion API, /account-deletion/me, and a user delete that keeps shared data).
Next agent: nothing is left to build in this lane: the code is complete and verified locally, including the branding on POST /me. Merge after ever-co/ever-gauzy#11519.
🤖 Generated with Claude Code
Summary by CodeRabbit
-
New Features
- Added a self-service account deletion flow with email verification, a review of affected accounts and workspaces, and explicit confirmation before deletion.
- Added account deletion from personal settings, including an impact preview and warnings when deletion may affect workspace administration.
- Added a link to request account deletion from sign-in pages and translated deletion-flow messages across supported languages.
- If the deletion plan changes before confirmation, the updated impact preview is shown for review.
-
Privacy
- Account deletion pages are excluded from search indexing and do not share referrer information. Analytics and error monitoring are not initialized on these pages.

















