Skip to content

feat: self-serve account deletion#270

Closed
cameronapak wants to merge 2 commits into
mainfrom
claude/self-serve-deletion-ordering-00be07
Closed

feat: self-serve account deletion#270
cameronapak wants to merge 2 commits into
mainfrom
claude/self-serve-deletion-ordering-00be07

Conversation

@cameronapak

Copy link
Copy Markdown
Owner

Summary

Adds the in-app "delete my account" flow the privacy policy commits to: a confirmed action that cancels any subscription, erases the outline, and deletes the account. Beta-blocking because the legal pages can't publish while they promise a flow that doesn't exist.

Fixes #224

Changes

  1. New Delete account item in the header More menu opens a type-to-confirm dialog; submitting sends a confirmation email and deletes nothing until the emailed link is clicked.
  2. The confirmation link runs an ordered teardown in Better Auth's beforeDelete — cancel Stripe → delete the D1 subscription row → wipe the outline Durable Object → delete the D1 identity — where a throw aborts before the point of no return, so "identity gone but Stripe still charging" is unreachable.
  3. Confirmation is by emailed link for every account (password and Google alike): once the verification sender is configured, that's the only path Better Auth's deleteUser route supports, and it's uniform + strongest — recorded as an ADR 0050 amendment.
  4. Post-delete, the account address is scrubbed from the waitlist and invites tables (best-effort), and the callback redirects the now-signed-out user to a "your account has been deleted" confirmation.
  5. New DO purge() RPC (deleteAll() wipes the whole colocated SQLite, then re-migrates to a valid empty schema); resolveUserId now shares one DO-name resolver with the delete path so the router and the teardown can't target different DOs.
  6. ADR 0050 records the design: immediate hard delete, the failure ordering, idempotency, refunds-decoupled, and honesty about the 30-day Durable Object PITR floor.

Start here: change 2 — the teardown ordering and the abort-before-identity-delete guarantee.

Flow

flowchart TD
  A[Confirm dialog sends email] --> B[User clicks emailed link]
  B --> C[beforeDelete: cancel Stripe]
  C --> D[delete D1 subscription row]
  D --> E[purge outline DO]
  E --> F[delete D1 identity + sessions]
  F --> G[afterDelete scrub, signed-out redirect]
Loading

Test plan

  • typecheck, typecheck:worker, typecheck:test, lint, bun test (665), build — all green
  • New unit tests: worker/delete-account.test.ts (DO-name resolution + Stripe-error abort/no-op classification)
  • Exercised end-to-end on cf:dev with a seeded user: dialog gating (button disabled until "DELETE"), email seam fired (correct From/To/Subject + callback URL), clicking the link ran the full teardown, D1 left with user/session/account/subscription all 0, landed signed-out with the confirmation toast, no server errors
  • Needs manual check: a paid user's Stripe cancellation actually hitting Stripe, and the Google-only account path — both need real Stripe test keys / a Google account, which the local mock can't exercise.

cameronapak and others added 2 commits July 15, 2026 08:37
Locks the design for #224 (beta-blocker): immediate hard delete built on
Better Auth deleteUser, with beforeDelete teardown ordered cancel Stripe ->
delete D1 subscription row -> wipe DO -> delete identity, so the
charged-but-deleted state is unreachable by construction. Ticket stays open
as the build ticket.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
More menu → Delete account: type-to-confirm, then an emailed confirmation
link whose callback runs the ordered teardown (ADR 0050) — cancel Stripe →
delete the D1 subscription row → wipe the outline DO → delete the D1 identity,
all in Better Auth's beforeDelete (a throw aborts before the point of no
return, so "identity gone but Stripe still charging" is unreachable).

- worker/delete-account.ts: teardown + pure guards (resolveDoName,
  shouldAbortOnStripeCancelError), unit-tested
- worker/outline-do.ts: purge() RPC (deleteAll wipes the DO's whole SQLite,
  then re-migrate to a valid empty schema)
- worker/auth.ts: enable user.deleteUser + hooks + confirmation email;
  email-confirm for everyone (Better Auth routes all deletes through it once
  the sender is configured — see ADR 0050 amendment)
- client: DeleteAccountDialog + More-menu entry + post-delete toast
- resolveUserId now delegates to the shared resolveDoName (no drift)

Verified end-to-end on cf:dev: dialog gating, email seam, full callback
teardown, D1 fully erased (user/session/account/subscription = 0), no errors.
Paid-Stripe + Google-token paths remain PR manual-checklist items.

Feeds #226 (privacy pages can publish the deletion + 30-day-backup language).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@cameronapak, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 31 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 150bd89f-3d0c-42d0-b23d-1f65926eab58

📥 Commits

Reviewing files that changed from the base of the PR and between 7c9cfd1 and aa232a0.

📒 Files selected for processing (11)
  • .changeset/self-serve-account-deletion.md
  • docs/adr/0050-self-serve-account-deletion.md
  • src/components/auth-screen.tsx
  • src/components/delete-account-dialog.tsx
  • src/components/header-more-menu.tsx
  • src/lib/auth-client.ts
  • worker/auth.ts
  • worker/delete-account.test.ts
  • worker/delete-account.ts
  • worker/index.ts
  • worker/outline-do.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/self-serve-deletion-ordering-00be07

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cameronapak

Copy link
Copy Markdown
Owner Author

Closing in favor of #268, which we're shipping for #224. #268 is the more complete implementation: it deletes the Stripe Customer (not just cancels subs), guards the owner-continuity account against self-delete, has a wider dunning-status sweep, scrubs OAuth rows too, and is e2e-tested including the re-signup no-resurrection check.

Two good ideas from this PR (ADR 0050) worth grafting into #268 later if wanted: email-link delete confirmation, and re-migrating the DO to a valid empty schema after deleteAll(). ADR 0051 (#268) already records that it supersedes the 0050 draft.

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.

Self-serve account deletion

1 participant