Allow users to self-delete their accounts in multiuser mode#70
Merged
Conversation
Adds a "Delete account" danger zone to the bottom of the page settings (Edit your page) in multiuser mode. Deletion opens a confirmation modal that requires typing the account's postcard address before the submit button enables, and the confirmation is re-verified server-side. AccountController#destroy cancels billing first by destroying the account's Pay::Customer records (Pay::Subscription's before_destroy cancels any active subscription at Stripe), then destroys the account, cascading to posts, subscribers, domains, imports, messages, visits, and feedbacks via dependent: :destroy. If billing cancellation fails, deletion aborts and the user is asked to contact support. The default Devise registration destroy is redirected to this flow in multiuser mode so deletion can't bypass confirmation and billing cleanup. Solo mode behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… job Billing: Pay's cancel-on-destroy callback swallows API errors and skips past_due subscriptions, either of which could leave a Stripe subscription billing forever after the local records were deleted. Instead, deletion now synchronously deletes the Stripe customer itself, which immediately cancels all of its subscriptions regardless of status and scrubs the customer's details from Stripe. Failures abort deletion and surface to the user. Deletion: established accounts can have hundreds of thousands of analytics, subscriber, and email rows, all previously destroyed row by row within the request. The controller now locks the account (blocking sign-in and taking the public page offline immediately) and enqueues DestroyAccountJob, which bulk-deletes the large tables in batches, purges visitor page-view events via the GIN index on ahoy_events.properties (previously orphaned), removes Pay records that the owner destroy never cleaned up, and deletes audit snapshots containing account PII. Also rewords the cancel-account copy to "Done with Postcard?". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds account self-deletion to the page settings in multiuser mode, with type-to-confirm friction, synchronous billing cancellation, and asynchronous full data cleanup.
User flow
pages/edit): a "Delete account" section shown only in multiuser mode and only to the account owner (not to admins viewing another account's settings).delete-accountStimulus controller): the user must type their postcard address (e.g.username.postcard.page) before the delete button enables. Closes on Escape or backdrop click. The confirmation is re-verified server-side (case/whitespace-insensitive).Billing cancellation (synchronous, fail-loud)
Pay's cancel-on-destroy callback is unsafe for this: it swallows API errors (logs and continues, so a failed Stripe call would delete local records while the subscription keeps billing) and its
active?check skipspast_duesubscriptions (a customer in dunning would keep getting retried after deletion). Instead,AccountController#destroydeletes the Stripe customer itself — Stripe immediately cancels all of a deleted customer's subscriptions regardless of status, and scrubs their details from Stripe. If the API call fails, deletion aborts with a contact-support message.Deletion (asynchronous)
Established accounts can have hundreds of thousands of analytics, subscriber, and email rows; the previous cascade destroyed them row by row (visits → events individually instantiated, audited subscriber destroys) — far too slow for a request. Now:
lock_access!) — sign-in is blocked and the public page 403s immediately — signs the user out, and enqueuesDestroyAccountJob.jsonb_path_opsindex onahoy_events.properties— these were previously orphaned), email messages, and subscriber records; destroysPay::Customerrecords (Pay never cleans these up on owner destroy); runsaccount.destroy!for the rest (posts, domains, imports, attachments) with normal callbacks; and finally purges audit snapshots, which contain account PII.Also
DELETE /accountsdestroy (no typed confirmation, no billing cleanup) now redirects to this flow in multiuser mode; solo mode unchanged.Tests
test/controllers/account_controller_test.rb(8 tests): lock + enqueue + sign-out on success, full data cascade when the job runs, case/whitespace-insensitive confirmation, wrong-confirmation rejection, solo-mode block, other-account block, auth requirement, Devise destroy redirect.test/jobs/destroy_account_job_test.rb: bulk deletion of visits, own and visitor analytics events, subscribers, email messages, and audits, while sharedEmailAddressrecords survive.rails test,rails zeitwerk:check, andbrakemanall pass.🤖 Generated with Claude Code