← All posts

What happens to your data when you delete your account

Account deletion is one of those features that seems straightforward and isn’t. Delete the account, delete everything associated with it — what’s hard about that?

What’s hard is doing it correctly across multiple databases and services, under requirements that pull in different directions: preserve data for audit trails and billing disputes, let people export before deleting, and handle users who belong to more than one organization. Here’s how we do it, and why the primary path is a cascade soft-delete.

What deletion actually means here

In smplkit, an account is an organization; a user is a person; one person can belong to several organizations (the agency consultant with multiple clients, the employee who had a personal account before the company bought an enterprise one). Deleting an account deletes the organization, not the people.

So the cascade removes the account itself, the membership rows, the environments, the API and SDK keys, the subscription and its usage records, and pending invitations. It deliberately does not remove user records — not even for people whose only account this was, because deleting them would break their verified email and make ever coming back needlessly painful. And product data in the other services’ databases is a separate story, below.

Cascade soft-delete in one transaction

The core path sets deleted_at = now() on the account and every directly-owned dependent record — account, account_user, environment, api_key, subscription, subscription_usage, service, invitation — in a single database transaction. Either the whole cascade lands or nothing does; there is no partial state where some records are deleted and others aren’t.

Soft-delete won over hard-delete for two reasons, and one of them has already paid for itself. Recovery: an accidental deletion should be reversible, and we’ve needed that exactly once so far — a test account deleted through the admin console — where undeleting was trivial instead of an incident. And the audit trail: when a billing dispute arrives six months after an account closed, we can still see exactly what the subscription looked like at deletion time. Hard-deleted records don’t.

Users survive their accounts

Deleting Account A can’t be allowed to touch User X’s membership in Account B, so the cascade soft-deletes account_user rows (memberships) and leaves user rows alone. Someone whose only account was deleted keeps their user record and their verified email; if they come back, they sign in, skip re-verification, and explicitly create a new account. We don’t create one for them automatically — a person who deleted their account may have meant it, and the polite thing is to take them at their word.

Product data: lazy, not eager

The transaction covers the app service’s database. Flag definitions, config entries, and logging records live in other services’ databases, and those are cleaned up lazily: deletion publishes an event, and each product service consumes it and soft-deletes its own account-scoped records at its own pace.

The eager alternative is a distributed transaction across every service, which requires all of them to be up at deletion time and fails into one of two bad states — a rolled-back deletion or a partial one. The lazy approach tolerates a service being down; the event just waits for it.

The cost is a window where a deleted account’s product data still exists in service databases. That window is harmless by construction: every product query is scoped by account ID at the application layer and again by row-level security at the database, a deleted account produces no valid tokens, and its API keys are rejected. The orphaned rows are visible to a DBA with database credentials and invisible to every path an external caller has. For compliance, soft-deleted records can be hard-deleted on a schedule after 90 days.

The edges

Invitations die with the account: pending invites are marked deleted in the cascade, and anyone clicking an old link gets a clean “This invitation is no longer valid.” We don’t notify pending invitees — at our current scale, the person finds out when they try.

Self-service deletion isn’t in the console yet — today deletion is an administrative action. An owner-initiated flow with a data-export step comes with the broader self-serve plan work. And some enterprise compliance regimes require “permanently deleted within 24 hours,” which soft-delete-then-eventually-hard-delete doesn’t satisfy; an immediate hard-delete path with cross-service synchronization is on the roadmap for the tier that needs it.

The shape of the whole design: one transaction where atomicity is possible, events where it isn’t, and a strong bias toward keeping recoverable records over trusting that nobody ever deletes the wrong thing. Somebody always deletes the wrong thing.