Skip to content

feat(api): Self-serve account deletion from Settings - #4600

Merged
junaway merged 12 commits into
release/v0.103.3from
feat/self-serve-account-deletion
Jun 12, 2026
Merged

feat(api): Self-serve account deletion from Settings#4600
junaway merged 12 commits into
release/v0.103.3from
feat/self-serve-account-deletion

Conversation

@mmabrouk

@mmabrouk mmabrouk commented Jun 9, 2026

Copy link
Copy Markdown
Member

Why

Users regularly ask us on chat to delete their accounts, and today an admin has to do it by hand. This adds a self-serve "Delete account" action in Settings, so users can remove themselves and we honor data-deletion requests without manual work.

What it does

A logged-in user opens Settings → Account, types their email to confirm, and deletes their account. DELETE /profile then runs, in this order:

  1. Blocks the delete with a 409 if the user solely owns an organization that has other members, so one person leaving cannot wipe a team's data.
  2. Cancels the Stripe subscription for each owned org (best effort).
  3. Deletes the SuperTokens login, before the DB cascade, so the idempotent signup override cannot recreate the account on the next sign-in.
  4. Runs the existing DB cascade (memberships + user + owned orgs).
  5. Removes the Loops contact (best effort).

The frontend signs the user out and redirects on success.

Scope is EE only (cloud and self-hosted EE). OSS keeps its shared singleton org, and the route returns 404 there.

How it reuses what exists

Most of this already existed; the PR mostly wires it together:

  • DB cascade reuses admin_delete_user_with_cascade + the membership cleanup the admin delete already runs.
  • New small pieces: emailing.delete_contact, an HTTP-free SubscriptionsService.cancel_stripe_subscription, count_organization_members, and PlatformAdminAccountsService.delete_own_account.

The existing self-serve DELETE /organizations/{id} has the same gap (it never cancels Stripe). Folding cancel_stripe_subscription into it is left as a follow-up.

Testing

api/ee/tests/pytest/acceptance/accounts/test_account_deletion.py covers two cases (happy-path delete, shared-org block). Both pass against the local EE stack.

Notes

  • Stacked on release/v0.103.0. The feature depends on emailing.py, which is introduced in v0.103.0. Retarget this PR to main once v0.103.0 lands.
  • Design docs live under docs/design/self-serve-account-deletion/.
  • deleteAccount() uses fetchJson to match the existing profile service. Migrate it to the Fern client once the OpenAPI spec is regenerated with the new DELETE /profile route.

@vercel

vercel Bot commented Jun 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jun 12, 2026 6:22pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3baad216-ce1e-4cc7-a3fe-f09806042809

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements self-serve account deletion for EE deployments alongside a subscription provisioning refactor. Backend changes include service orchestration across SuperTokens, Stripe, and Loops; an EE-gated DELETE /profile endpoint; and acceptance tests. Frontend adds Settings sidebar Account tab and a deletion confirmation modal. Supporting infrastructure includes member counting, interactive session detection, and contact removal helpers. Documentation covers design decisions and implementation verification.

Changes

Self-Serve Account Deletion

Layer / File(s) Summary
Domain error contracts
api/oss/src/core/accounts/errors.py
New AccountHasMembersError and AccountAuthDeletionError classes handle account deletion failure modes with structured details and messages.
Infrastructure helpers
api/oss/src/middlewares/auth.py, api/ee/src/services/db_manager_ee.py, api/oss/src/utils/emailing.py
Three independent utilities: is_interactive_session() distinguishes SuperTokens auth from API keys; count_organization_members() counts org members; remove_contact() removes Loops contacts with exponential backoff retry.
Service orchestration
api/oss/src/core/accounts/service.py
delete_own_account() validates user, checks EE member constraints, cancels Stripe subscriptions per owned org, deletes SuperTokens auth (required), cascades user/memberships/orgs, and best-effort removes Loops contact.
API endpoint and HTTP routing
api/oss/src/routers/user_profile.py
DELETE / endpoint with EE gating, interactive-session requirement, domain error-to-HTTP mapping (409 for shared orgs, 502 for auth failure, etc.), cache invalidation, and success response.
Backend acceptance tests
api/ee/tests/pytest/acceptance/accounts/test_account_deletion.py
Three test scenarios covering successful deletion with email reuse, deletion blocked when owned org has members, and API-key auth rejection; includes helpers for account creation with optional API keys and SuperTokens sign-in.
Frontend profile service
web/oss/src/services/profile/index.ts
deleteAccount() async function calling DELETE /profile endpoint.
Settings sidebar Account tab
web/oss/src/components/Sidebar/SettingsSidebar.tsx
EE-conditional Account menu item, canShowAccount flag, tab gating, and dependency list updates.
DeleteAccount component
web/oss/src/components/pages/settings/Account/DeleteAccount.tsx
Modal UI with email typed-entry validation, useMutation wiring for deletion, success toast and logout flow, error handling.
Settings page integration
web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx
Dynamic DeleteAccount import, tab gating based on EE eligibility, label mapping, and tab content rendering.
Design documentation
docs/design/self-serve-account-deletion/*
Five design documents plus status report covering scope, context, decisions, implementation plan, research conventions, and verification.

Subscription Provisioning Refactoring

Layer / File(s) Summary
SubscriptionsService refactoring
api/ee/src/core/subscriptions/service.py
Rename provision_signup_subscription() to provision_subscription() as single decision point; add cancel_subscription() for best-effort Stripe cancellation.
Signup flow update
api/ee/src/services/commoners.py
Call renamed provision_subscription() in create_organization_for_signup().
Documentation updates
docs/design/ee-self-hosting/plan.md, docs/design/ee-self-hosting/rfc-0.md, docs/design/ee-self-hosting/status.md
Reflect provision_subscription() as the centralized Stripe-vs-self-hosted branching point in design docs.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.83% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(api): Self-serve account deletion from Settings' clearly and concisely summarizes the main change—adding a self-serve account deletion feature in the Settings UI.
Description check ✅ Passed The description is highly detailed and directly related to the changeset, explaining the motivation, implementation order, scope, testing, and integration with existing systems.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/self-serve-account-deletion

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (1)
api/ee/tests/pytest/acceptance/accounts/test_account_deletion.py (1)

100-106: ⚡ Quick win

Assert the 409 payload contract, not just the status code.

Please assert that the conflict response includes the structured account-deletion error payload (code/details/organizations). This will lock the cross-layer contract and catch regressions early.

Suggested assertion pattern
         blocked = requests.delete(
             f"{api_url}/profile",
             headers={"Authorization": owner_creds},
             timeout=BASE_TIMEOUT,
         )
         assert blocked.status_code == 409, blocked.text
+        payload = blocked.json()["detail"]
+        assert payload["code"] == "account_has_members"
+        assert payload["details"]["organizations"]

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a83bb4f0-b96e-4633-b90a-f63cebb548bb

📥 Commits

Reviewing files that changed from the base of the PR and between 6d3368c and 47997f4.

📒 Files selected for processing (17)
  • api/ee/src/core/subscriptions/service.py
  • api/ee/src/services/db_manager_ee.py
  • api/ee/tests/pytest/acceptance/accounts/test_account_deletion.py
  • api/oss/src/core/accounts/errors.py
  • api/oss/src/core/accounts/service.py
  • api/oss/src/routers/user_profile.py
  • api/oss/src/utils/emailing.py
  • docs/design/self-serve-account-deletion/README.md
  • docs/design/self-serve-account-deletion/context.md
  • docs/design/self-serve-account-deletion/decisions.md
  • docs/design/self-serve-account-deletion/plan.md
  • docs/design/self-serve-account-deletion/research.md
  • docs/design/self-serve-account-deletion/status.md
  • web/oss/src/components/Sidebar/SettingsSidebar.tsx
  • web/oss/src/components/pages/settings/Account/DeleteAccount.tsx
  • web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx
  • web/oss/src/services/profile/index.ts

Comment thread api/oss/src/core/accounts/errors.py
Comment thread api/oss/src/core/accounts/service.py
Comment thread api/oss/src/routers/user_profile.py
Comment thread api/oss/src/utils/emailing.py Outdated
Comment thread docs/design/self-serve-account-deletion/plan.md Outdated
Comment thread docs/design/self-serve-account-deletion/plan.md Outdated
Comment thread docs/design/self-serve-account-deletion/research.md Outdated
Comment thread web/oss/src/components/pages/settings/Account/DeleteAccount.tsx Outdated
Comment thread web/oss/src/services/profile/index.ts
mmabrouk added 3 commits June 10, 2026 22:25
Add a self-serve "Delete account" flow so users can remove their own
account instead of asking support. EE-only (cloud and self-hosted EE);
OSS keeps its shared singleton org and does not expose the route.

DELETE /profile deletes the caller, the organizations they solely own,
their SuperTokens login, their Stripe subscription, and their Loops
contact, in that order. SuperTokens is deleted before the DB cascade so
the idempotent signup override cannot recreate the account on next login.
If the user owns an org with other members, the request is blocked (409)
rather than deleting the team's data.

Reuses the existing admin cascade (admin_delete_user_with_cascade +
membership cleanup). New pieces: emailing.delete_contact, a HTTP-free
SubscriptionsService.cancel_stripe_subscription, count_organization_members,
and PlatformAdminAccountsService.delete_own_account. Frontend adds an
EE-only Account tab in Settings with a type-your-email confirm modal that
signs the user out on success.

Acceptance tests (happy path + shared-org block) pass against the EE stack.
@mmabrouk
mmabrouk force-pushed the feat/self-serve-account-deletion branch from 7f8c8bd to c9152c2 Compare June 10, 2026 20:25
@mmabrouk
mmabrouk changed the base branch from release/v0.103.0 to main June 11, 2026 09:30
@mmabrouk

Copy link
Copy Markdown
Member Author

@jp-agenta don't forget me :)

@junaway
junaway marked this pull request as ready for review June 12, 2026 09:57
Copilot AI review requested due to automatic review settings June 12, 2026 09:57
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. feature labels Jun 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/ee/src/core/subscriptions/service.py (1)

202-236: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make provision_subscription() fail hard when no subscription row is created.

start_reverse_trial() and start_plan() still return None on write/provisioning failures, and create_organization_for_signup() doesn't inspect the result. That means signup can persist a new organization with no subscription record and still continue into the entitlement path. Treat a missing result as an error here so the existing rollback path runs instead of leaving partial state.

Proposed fix
-    async def provision_subscription(
+    async def provision_subscription(
         self,
         *,
         organization_id: str,
         organization_name: str,
         organization_email: str,
-    ) -> Optional[SubscriptionDTO]:
+    ) -> SubscriptionDTO:
@@
         if env.stripe.enabled:
             if trial_enabled():
-                return await self.start_reverse_trial(
+                subscription = await self.start_reverse_trial(
                     organization_id=organization_id,
                     organization_name=organization_name,
                     organization_email=organization_email,
                 )
+                if subscription is None:
+                    raise EventException(
+                        f"Failed to provision subscription for organization ID: {organization_id}"
+                    )
+                return subscription
 
             free_plan = get_free_plan()
             log.info(
                 "Trial not configured; onboarding org %s on free plan [%s]",
                 organization_id,
                 free_plan,
             )
-            return await self.start_plan(
+            subscription = await self.start_plan(
                 organization_id=organization_id,
                 plan=free_plan,
             )
+            if subscription is None:
+                raise EventException(
+                    f"Failed to provision subscription for organization ID: {organization_id}"
+                )
+            return subscription
 
-        return await self.start_plan(
+        subscription = await self.start_plan(
             organization_id=organization_id,
             plan=get_default_plan(),
         )
+        if subscription is None:
+            raise EventException(
+                f"Failed to provision subscription for organization ID: {organization_id}"
+            )
+        return subscription

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8444151f-d266-4437-b576-d1e10dc4704d

📥 Commits

Reviewing files that changed from the base of the PR and between 47997f4 and 2875b68.

📒 Files selected for processing (22)
  • api/ee/src/core/subscriptions/service.py
  • api/ee/src/services/commoners.py
  • api/ee/src/services/db_manager_ee.py
  • api/ee/tests/pytest/acceptance/accounts/test_account_deletion.py
  • api/oss/src/core/accounts/errors.py
  • api/oss/src/core/accounts/service.py
  • api/oss/src/middlewares/auth.py
  • api/oss/src/routers/user_profile.py
  • api/oss/src/utils/emailing.py
  • docs/design/ee-self-hosting/plan.md
  • docs/design/ee-self-hosting/rfc-0.md
  • docs/design/ee-self-hosting/status.md
  • docs/design/self-serve-account-deletion/README.md
  • docs/design/self-serve-account-deletion/context.md
  • docs/design/self-serve-account-deletion/decisions.md
  • docs/design/self-serve-account-deletion/plan.md
  • docs/design/self-serve-account-deletion/research.md
  • docs/design/self-serve-account-deletion/status.md
  • web/oss/src/components/Sidebar/SettingsSidebar.tsx
  • web/oss/src/components/pages/settings/Account/DeleteAccount.tsx
  • web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx
  • web/oss/src/services/profile/index.ts
✅ Files skipped from review due to trivial changes (3)
  • docs/design/self-serve-account-deletion/README.md
  • docs/design/ee-self-hosting/status.md
  • docs/design/ee-self-hosting/plan.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • web/oss/src/services/profile/index.ts
  • api/oss/src/core/accounts/errors.py
  • web/oss/src/components/Sidebar/SettingsSidebar.tsx
  • web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx
  • web/oss/src/components/pages/settings/Account/DeleteAccount.tsx

Comment thread api/oss/src/utils/emailing.py Outdated
Comment thread docs/design/ee-self-hosting/rfc-0.md
Comment thread docs/design/ee-self-hosting/rfc-0.md Outdated
Comment thread docs/design/self-serve-account-deletion/plan.md
Comment thread docs/design/self-serve-account-deletion/research.md
Comment thread docs/design/self-serve-account-deletion/status.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an EE-only self-serve “Delete account” flow exposed in Settings, backed by a new authenticated DELETE /profile endpoint that orchestrates Stripe cancellation (best-effort), SuperTokens identity deletion (required), DB cascade deletion, and Loops contact removal (best-effort). It fits into the existing accounts/admin-delete infrastructure by reusing the existing cascade deletion and adding a small amount of orchestration + UI wiring.

Changes:

  • Adds DELETE /profile (EE-gated) with an “interactive session only” guard to prevent API keys from deleting accounts.
  • Implements backend orchestration for self-serve account deletion (shared-org membership guard, Stripe cancel, SuperTokens deletion, cascade DB delete, Loops removal) plus acceptance tests.
  • Adds an EE-only “Account” settings tab with a typed-email confirmation modal and a new deleteAccount() client call.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
web/oss/src/services/profile/index.ts Adds deleteAccount() service call to invoke account deletion.
web/oss/src/pages/w/[workspace_id]/p/[project_id]/settings/index.tsx Adds an EE-only “Account” tab and wires the new settings content.
web/oss/src/components/Sidebar/SettingsSidebar.tsx Adds an EE-only “Account” entry to the settings sidebar navigation.
web/oss/src/components/pages/settings/Account/DeleteAccount.tsx Implements the “Delete account” UI with typed-email confirmation and mutation/logout flow.
docs/design/self-serve-account-deletion/README.md Introduces design workspace for the feature.
docs/design/self-serve-account-deletion/context.md Documents goals/non-goals and motivation.
docs/design/self-serve-account-deletion/research.md Captures prior-art and codebase references for deletion mechanics.
docs/design/self-serve-account-deletion/plan.md Documents intended design and implementation plan.
docs/design/self-serve-account-deletion/decisions.md Records design decisions (EE-only, shared-org guard, ordering, etc.).
docs/design/self-serve-account-deletion/status.md Tracks implementation status and follow-ups.
docs/design/ee-self-hosting/status.md Updates naming reference to provision_subscription().
docs/design/ee-self-hosting/rfc-0.md Updates naming reference to provision_subscription().
docs/design/ee-self-hosting/plan.md Updates naming reference to provision_subscription().
api/oss/src/utils/emailing.py Adds Loops remove_contact() helper (best-effort deletion).
api/oss/src/middlewares/auth.py Adds is_interactive_session() helper for interactive-session gating.
api/oss/src/routers/user_profile.py Adds DELETE /profile endpoint (EE-only + interactive-session guard + exception mapping).
api/oss/src/core/accounts/errors.py Adds typed domain errors for self-serve deletion failure modes.
api/oss/src/core/accounts/service.py Adds delete_own_account() orchestration and SuperTokens deletion helper.
api/ee/src/services/db_manager_ee.py Adds count_organization_members() for shared-org guard.
api/ee/src/services/commoners.py Renames callsite to provision_subscription().
api/ee/src/core/subscriptions/service.py Renames provision_signup_subscription()provision_subscription(); adds cancel_subscription() used by deletion.
api/ee/tests/pytest/acceptance/accounts/test_account_deletion.py Adds acceptance tests for happy path, shared-org block, and API key rejection.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread api/oss/src/core/accounts/service.py Outdated
Comment thread api/oss/src/core/accounts/service.py Outdated
Comment thread api/oss/src/core/accounts/service.py
@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Preview URL https://gateway-production-6949.up.railway.app/w
Project agenta-oss-pr-4600
Image tag pr-4600-c51751a
Status Deployed
Railway logs Open logs
Workflow logs View workflow run
Updated at 2026-06-12T18:49:55.417Z

@bekossy
bekossy changed the base branch from main to release/v0.103.3 June 12, 2026 11:28
@CLAassistant

CLAassistant commented Jun 12, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Account deletion 500'd for users who had accepted an invitation into
another organization. Accepting an invite stamps the host org's
project_invitations.user_id, and that FK (like the modified_by_id audit
columns and webhook_subscriptions.created_by_id) has no ON DELETE rule.
The host org survives the cascade, so DELETE FROM users hit a foreign
key violation after SuperTokens and Stripe were already processed, and
the frontend never ran the logout flow.

Clear those references inside the same transaction before the user row
is deleted: drop the user's invitation rows and webhook subscriptions
(created_by_id is NOT NULL), and null the modified_by_id audit columns.
This also fixes the same latent bug in the admin delete path.

Regression test drives the real invite, accept, delete flow.
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jun 12, 2026
@junaway
junaway merged commit fa31f06 into release/v0.103.3 Jun 12, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature lgtm This PR has been approved by a maintainer size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants