DEV Community

Posting Dude
Posting Dude

Posted on

Someone Asked Me to Delete Their Account. My Tiny SaaS Wasn't Ready.

The first "please delete my account and all my data" email I got was from a guy who'd signed up, poked around for four minutes, and left. Easy, I thought. One row in users, gone.

It took me most of an evening. Not because the delete was hard. Because I didn't actually know where his data was.

If you run a small SaaS, this email is coming. Under GDPR you have one month to act on it, and "I'm a solo founder" isn't an exemption. Here's the setup I wish I had before that first one.

1. Write down every place a user's data lives

Not your schema. Every place. Mine, when I finally listed it:

  • the main Postgres tables (users, workspaces, the stuff they created)
  • Stripe customer and invoices
  • the email tool (signup list + transactional logs)
  • product analytics events
  • error tracker (yes, emails and IDs end up in Sentry breadcrumbs)
  • uploaded files in object storage
  • support inbox threads
  • nightly database backups

That list is the actual work. Put it in a markdown file in the repo. Every time you add a new vendor, add a line. It takes two minutes and saves the evening.

2. Decide what "delete" means per place, before anyone asks

You can't delete everything, and you shouldn't pretend to.

  • Delete: app data, uploads, analytics profile, email list membership.
  • Keep, but say so: invoices. Tax law usually wants those kept for years. Stripe lets you delete a customer object, but your accounting records stay. That's allowed, just tell the user.
  • Let it age out: backups. Nobody restores a 30-day-old backup to scrub one row. Write down your retention window and make sure that if you ever restore, you re-run deletions. I keep deleted user IDs in a tiny deletion_log table (ID + date, nothing else) for exactly that.

I wrote about testing restores in my monthly backup drill post. Deletion is the other half of that: if you restore, deleted people must stay deleted.

3. Soft delete first, hard delete on a timer

My flow now:

  1. User clicks delete (or emails). I mark deleted_at, log them out everywhere, cancel the subscription.
  2. 14 days later a scheduled job hard-deletes the rows and files and calls the vendor APIs.
  3. The deletion_log row is written, and they get one plain email: "done, here's what we kept and why."

The 14 days are because about one in five of these is someone who regrets it the next morning. Restoring a soft-deleted account is one UPDATE. Restoring a hard-deleted one is impossible.

4. Cascades will surprise you

First time I ran the hard delete, it failed on a foreign key from a comments table I forgot existed. Second time I'd added ON DELETE CASCADE everywhere and it nearly wiped a shared workspace other people were in.

So: decide ownership. If a user owns a workspace alone, it goes. If others are in it, ownership transfers or their content is anonymized ("Deleted user") instead of removed. Run the delete inside a transaction against a copy of prod first and look at the row counts before you trust it.

I do that kind of check with saved read-only SQL instead of a fancy admin page, roughly the setup in this post about skipping the admin panel.

5. Give them an export button too

Same law, other right: people can ask for a copy of their data. A JSON dump of their own rows plus a zip of their uploads is enough at our size. It's also a nice thing to offer on the delete screen, "download your stuff first?". A few people actually use it, and it makes the whole exit feel less hostile.

What it costs

For me: one markdown file, one table, one scheduled job, maybe a day of work total. The second deletion request took eleven minutes, most of it writing the email.

Small products win trust in boring places like this. If you're getting ready to put something new in front of people, I list early launches on Aura++, and the founders who get the boring stuff right are the ones I end up recommending.

How do you handle deletion requests right now? Manual SQL, a script, or "I'll deal with it when it happens"?

Top comments (0)