Delete test accounts
backend/scripts/delete-accounts.ts removes, per target, the tenants row (every tenant-scoped table cascades from it) and the auth.users row (Supabase's auth tables cascade from it). Targets are auth user UUIDs or emails. Default is a dry-run inventory; --apply deletes — and only when every target is deletable: one BLOCKED or not found target aborts the whole run with nothing deleted, so a typo cannot produce a partial cleanup.
A target is BLOCKED when its membership is not owner, when the tenant has other members, or when the tenant still records a Stripe subscription and no STRIPE_SECRET_KEY is available. With STRIPE_SECRET_KEY in the env file the subscription is canceled before the delete (same call and tolerance as the in-app DELETE /me/account).
Procedure (production)
Production requires user approval for every DB operation, dry-run included (docs/tasks.local.md §prod DB). Two approvals, one per command:
- Inventory (read only) — present the command, get OK, run:
Show the output verbatim: email, tenant id / name, plan, role / member count, Stripe ids, first MCP connection, row counts. Confirm every target is the intended test account (email, created date). Stop if a target iscd backend && npx tsx scripts/delete-accounts.ts --env-file=.env.production <uuid|email>...BLOCKEDor looks like a real user;--applyrefuses to run until the list is clean. - Delete (write) — only after the user confirms the inventory:
Report thecd backend && npx tsx scripts/delete-accounts.ts --env-file=.env.production <uuid|email>... --applydeleted <target>: tenants=N, auth.users=Nlines.
Local stack: omit --env-file (falls back to backend/.dev.vars).
Not covered
- Stripe without
STRIPE_SECRET_KEY: a tenant that records astripe_subscription_idis refused. Afreeplan does not prove the subscription is dead — the webhook keeps the id for statuses that can still bill (incomplete,past_due). Put the key in the env file, or have the user delete through the app. - Stripe customer objects are left in place; only the subscription is canceled.
- MCP OAuth tokens in KV (
MCP_OAUTH_STORE) expire on their own (≤30 days) and Google OAuth grants are not revoked — same as the in-app deletion. account_deletion_surveysis not written: ops cleanup is not user churn.