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 a system with multiple databases, multiple services, and requirements around data preservation (legal holds, audit trails), data portability (let users export before deleting), and user accounts that span multiple organizations.
This post describes how we handle account deletion, why we chose cascade soft-delete for the primary path, and what “orphaned” product data actually means in our architecture.
The Multi-Tenant Data Topology
In smplkit’s architecture, an account is an organization — a company or team that signs up for smplkit. A user is a person. One user can belong to multiple accounts (an agency consultant working with multiple clients, an employee who created a personal account before their company got an enterprise account).
When you delete an account, you’re deleting the organization, not the person. Deleting the account should remove:
- The account itself
- All account members (the account_user join table rows)
- All environments belonging to the account
- All API keys and SDK keys for the account
- The subscription and subscription usage records
- Pending invitations
It should NOT delete:
- The user records for people who were members of the account, if they also belong to other accounts
- The user records for people who were members of the account, if they don’t belong to other accounts — deleting these would make re-registration painful and break email verification status
And separately, it should handle:
- Product data (flags, config entries, logging records) that lives in separate service databases
Cascade Soft-Delete in One Transaction
The core account deletion path is a cascade soft-delete: we set deleted_at = now() on the account record and on all directly-owned dependent records in a single database transaction.
The transaction touches: account, account_user, environment, api_key, subscription, subscription_usage, service, invitation. Everything in the app service’s database. Everything is soft-deleted atomically.
Soft-delete over hard-delete for two reasons:
Recovery. Accidental account deletion by an admin or a cascading bug should be reversible. Hard-deleted records are gone. Soft-deleted records can be undeleted. We’ve had to do this exactly once in production (a test account accidentally deleted via the admin console) and the soft-delete made recovery trivial.
Audit trail. When a customer’s billing dispute lands in our inbox six months after account closure, we need to be able to see what their subscription state was at the time of deletion. Soft-deleted records preserve this. Hard-deleted records don’t.
The single-transaction guarantee means that account deletion is atomic. Either the whole cascade succeeds or nothing is deleted. There’s no partial state where some records are deleted and others aren’t.
User Preservation
Users who belong to multiple accounts need careful handling. Deleting Account A shouldn’t affect User X’s membership in Account B.
The cascade soft-delete touches account_user rows (the membership records) but not user rows. After account deletion, users who had only that one account still have their user record. They can re-add a payment method, create a new account, and keep their verified email address without going through email verification again.
The alternative — deleting user records when their last account is deleted — creates friction that makes account re-creation painful. There’s no user-facing reason to do this. User records are cheap to store and valuable to preserve.
Product Data: The Cross-Service Problem
Account deletion in the app service handles the app service’s database. It doesn’t touch product service databases (the flags database, the config database, the logging database). These contain flag definitions, config entries, logging records, etc. — all scoped to the deleted account.
We made a deliberate decision here: product data deletion is handled lazily, not eagerly.
When an account is deleted, a deletion event is published. Product services consume this event and soft-delete their own account-scoped records at their own pace. This is eventual consistency: there’s a window after account deletion where product service databases still contain records for the deleted account.
Why not delete synchronously in a distributed transaction? Distributed transactions across services are fragile — they require all services to be available at deletion time, and a failure in any service either rolls back the deletion (bad) or leaves partial deletes (also bad). The eventual consistency approach is more resilient: if the flags service is temporarily unavailable, the deletion event will be processed when it recovers.
Why is orphaned product data “harmless”? Because product service queries are scoped by account ID (enforced at the application layer and by row-level security at the database layer). Soft-deleted account records don’t produce valid auth tokens. Soft-deleted api_keys are rejected. There’s no path by which an external caller can access orphaned product data after account deletion.
The orphaned data is visible at the database layer (a DBA querying the flags database directly would see rows for the deleted account) but not at the API layer. For compliance purposes, we can schedule periodic hard-deletion of soft-deleted records older than 90 days.
What About the User Who’s a Member of Only One Account?
This is the simple case. User U has only one account membership. The account is deleted. U’s account_user row is soft-deleted. U’s user row is preserved.
If U wants to use smplkit again, they can sign up with the same email address. They’ll already have a verified email (from before the deletion), so they skip the email verification step. They create a new account from scratch.
We don’t automatically create a new account for U when they next log in. A user who deleted their account might not want a new one — they might have left smplkit on purpose. We require the explicit “create account” action.
Invitation Handling
Invitations have a slightly different lifecycle. A pending invitation to join the deleted account becomes invalid when the account is soft-deleted. We mark the invitation as deleted in the cascade. If someone tries to accept an invalid invitation, they get a clean error: “This invitation is no longer valid.”
We don’t send notifications to pending invitees when an account is deleted. The invitee finds out when they try to use the invitation link. This is acceptable behavior for a platform that doesn’t have a critical mass of users for whom getting this notification would be impactful.
What We’d Revisit
Self-service account deletion in the developer console. A signed-in owner should be able to delete their account directly from the console, including a data export step beforehand. We’ll wire this up alongside the broader self-serve plan management work.
Immediate hard-deletion path for enterprise. Some enterprise customers need immediate hard deletion for data-residency compliance. Soft-delete with eventual hard-delete doesn’t satisfy “permanently deleted within 24 hours,” so an enterprise hard-delete path with cross-service synchronization is on the roadmap.
smplkit account deletion is a cascade soft-delete across the app service database in a single transaction, with eventual deletion of product service data via events. User records are preserved across account deletion; membership records are not.