Repository navigation
feat(account): data export and account deletion - #83
Conversation
Settings gets "Download my data" and "Delete account". Both call a new `account` Edge Function that acts only on the caller's own account. Export returns the auth profile and every row reached through a foreign key into auth.users, plus chat messages and announcements, with credential columns left out. Deletion cancels live Stripe subscriptions immediately and stops if Stripe fails, removes the user's Storage objects, clears their workspaces, then deletes the auth user. The migration makes the other blocking foreign keys cascade or set null, and a trigger clears announcement authors and recipients. Workspace foreign keys stay RESTRICT, so a raw auth delete still cannot remove shared work. A pgTAP test seeds a row in every user-owned table and checks that no public or private row mentions the user after deletion. A full-stack spec does the same through the real UI, Auth, and Edge Function, and checks that one user cannot export or delete another. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Select Storage objects for deletion only by their Storage owner. A user could point a source_files row at another user's object, and the service role would have deleted it. - Export admin grants as type and dates only, and skip admin catalog tables. The generic export returned grant notes that RLS hides from the user. - Record Stripe customers in a service-role-only stripe_customers table when Checkout opens, and expire open Checkout sessions before canceling subscriptions. Deletion no longer depends on the webhook having recorded a subscription. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… rows - Search Stripe for customers by metadata.user_id during deletion, in addition to recorded ones, so customers created before stripe_customers existed are still stopped. A failed search stops deletion. The local gate's fake answers the search through the new STRIPE_API_URL override. - Export only rows whose ownership column references the user. Columns such as resolved_by and actor_id pulled in rows other people own. - Leave scheduled and withdrawn announcements out of the export, as the read policy does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reachedEnable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Next included review available in 2 seconds. View limit detailsLimit details: You’ve used the included review currently available. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Free Run ID: 📒 Files selected for processing (21)
Note 🎁 Summarized by CodeRabbit FreeYour organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing. Comment |
|
@coderabbitai full review |
|
- Export workspace rows only while the user is still a member of that workspace, as the read policies require. A removed member could otherwise download shared work edited after their removal. - Treat resource_missing as "customer gone" only when listing. A session or subscription that disappears mid-cleanup is now skipped on its own, so the customer's other subscriptions still get canceled. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@coderabbitai full review |
|
private.workspace_invite_attempts records the actor's user id without a foreign key, so deleting an account left those rows behind. The deletion test never seeded one, so its "no row mentions the user" check could not see the gap. Seed an attempt and clear it in prepare_account_deletion. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Deleting a leaver's invite attempts also deleted the failures the per-IP limit counts, so deleting an account and signing up again reset that limit. Anonymize the rows instead: the actor becomes the nil UUID, the IP failures still count, and the redeem function purges them after a day as before. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@coderabbitai review |
|
Brings in #79 to #83 and #85. Conflict in .superset/db-gate.sh: both sides added a function to the readiness loop. The loop now waits for both `voice` and `account`. #82 replaced the "Which debate is this for?" dialog with the first-debate offer when no debate is current. e2e/voice-rounds.spec.ts still clicked the old button in two places, so it answers the new offer the way e2e/sparring.spec.ts does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
What this adds
Settings now has Download my data (in the Account card) and Delete account (in the Danger Zone, beside the existing Delete All My Data, which keeps the account). Both call a new
accountEdge Function. It acts only on the caller, taking the user ID from the verified session and never from the request body.Export returns one JSON file with the auth profile plus every row whose ownership column (
user_id,owner_id,author_id,created_by,recipient_id) references the user, chat messages, published announcements addressed to the user, and announcements they wrote as an admin. It finds tables through the catalog, so a new user-owned table is exported automatically. Columns that only mention the user, such asactor_idorresolved_by, never select a row, and workspace rows are exported only while the user is still a member, as their read policies require. It leaves out API key ciphertext, sparring claim tokens, audit request fingerprints, and admin catalog work (promo codes, curated sources, global prompts, demo codes). Admin grants appear as grant type and dates only, because RLS hides their notes and author from the user.Delete asks the user to type their email (or DELETE for an account without one), then runs in this order:
subscriptions, in the newstripe_customerstable, or found by a Stripe search onmetadata.user_id. The function expires open Checkout sessions, then cancels live subscriptions immediately, without a refund. If Stripe fails, it stops and deletes nothing.source_files.storage_keyis user-writable, so it never selects an object for deletion.prepare_account_deletiondeletes workspaces the user owns and their sources, threads, messages, and results in other workspaces.Why deletion cancels Stripe instead of refusing
Refusing while a subscription is active would leave a user who already canceled at period end unable to delete until the period ran out. Canceling first and stopping on any Stripe error means the function never deletes an account that Stripe still bills. Stripe keeps its own invoices and payments for tax and accounting. The dialog, the Privacy page, and
docs/architecture.mdall say so.Schema changes (migration
20261001100000)promo_codes.created_byandcurated_sources.created_by: change fromNO ACTIONtoSET NULL. Before, they blocked deleting any admin who had created one.shared_items.created_by: changes fromSET NULLtoCASCADE, so public share links to the user's work stop working.admin_announcements.created_bybecomes nullable. ABEFORE DELETEtrigger onauth.usersclears it and removes the user fromtarget_user_ids, deleting an announcement left with no recipients.src/lib/announcements.tsnow accepts a null author.RESTRICT.workspaces_rls_testasserts that a raw delete cannot remove an owner, and that still holds; only the account path clears them first.stripe_customerstable, service role only.create-checkoutrecords the customer there before it opens a session and fails closed if it can't, so deletion finds a subscription the webhook hasn't recorded yet.export_account_data,account_deletion_plan,prepare_account_deletion.Workspace is hidden in production. Deleting an account deletes the workspaces it owns, even ones with other members. If Workspace ships, transferring ownership is the alternative.
Codex review
Four Codex review rounds (
gpt-6.1-sol, high effort) ran on this branch.The first found three P1 issues, fixed in the second commit. A user could get another user's Storage object deleted by naming it in their own
source_filesrow. The export returned admin grant notes that RLS hides. Deletion could miss a subscription created between Checkout and the webhook.The second found two P1s and two P2s. Three are fixed in the third commit: Stripe customers created before
stripe_customersexisted (now found by Stripe search), export rows selected through non-ownership columns likeresolved_by, and scheduled announcements in the export.One is accepted, not fixed. A Checkout session opened in another tab during the few seconds deletion runs is not stopped. Only the user deleting their own account can trigger it, and closing it would need a lock shared by
create-checkoutandaccount.The third found two P1s, both fixed in the fourth commit. A removed workspace member could still export shared rows they had created. Separately, a
resource_missingerror on a single Checkout session or subscription skipped that customer's remaining subscriptions. The pgTAP test and Deno tests now cover both.The fourth ran on the final head,
894c063, and found no actionable defects. It read the full diff and the migrations it touches, and ran typecheck, the Settings tests, and the account function tests.Tests
supabase/tests/account_deletion_test.sql(27 assertions, including a removed workspace member) gives the leaving user a row in every table with a foreign key toauth.users. It fails if a future table is missing from the fixture or from the export, then scans every row of everypublicandprivatetable as text for the user ID after deletion. It also checks that the other user keeps their data and that onlyservice_rolecan call the RPCs.e2e/full-stack/account-deletion.spec.tsdrives the real Settings UI, Auth, and Edge Function. It downloads the export, deletes the account, then scans Postgres directly for the user ID. It checks that a request naming another user returns only the caller's data, that another user's confirmation deletes nothing, that calls without a session get 401, and that deletion searched Stripe for the user. The gate's fake provider answers that search throughSTRIPE_API_URL, which only the gate sets.supabase/functions/account/index_test.tscovers step order, stopping on a Stripe or Storage failure, confirmation matching, which subscription states get canceled, and customers found only by Stripe search.src/test/pages/SettingsPage.test.tsxcovers the dialog's exact-match gate and the failure path.No test calls real Stripe, because the local key is a placeholder. The Deno tests run checkout expiry and cancellation against a fake.
Verification at
894c063CI, on clean runners:
qualitypassed: docs, lint, typecheck, unit, collaboration, Edge, and build.database-testspassed: every pgTAP file, includingaccount_deletion_test, then 59 full-stack browser tests with the one existing mobile skip. Both account journeys ran on desktop and mobile Chromium.browser-testspassed.live-e2efails on AI-provider timeouts against live OpenRouter and Exa. This change doesn't touch those paths, and the same workflow fails onmainand on other branches today.Locally, the account Deno tests (15) and the affected pgTAP files passed at this head. Earlier in the branch,
npm test(1110 tests) andnpm run test:edge(283 tests) passed. The local browser stage was unreliable. Something outside the gate scripts randocker rm -fon the shareddebatelab-fake-openroutercontainer mid-run (Docker recorded SIGKILL, exit 137).Deploy notes
Merging ships the frontend only. The migration, the new
accountfunction, and the updatedcreate-checkoutneed a separate, authorized deploy, with the migration first. Until the backend is live, the two new Settings buttons return an error.🤖 Generated with Claude Code