Back to Engineering Blog

GDPR User Deletion in Practice: Apps, Vendors, and Backups

A practical account deletion design for auth, application data, analytics, vendors, and backups, with the limits of anonymization made explicit.

kristian

kristian

Engineer

10 min read
#engineering#privacy#gdpr#backend
A protected user token connected to services while identifying data is removed

Deleting an account is not one database statement. A user can leave traces in authentication tables, application records, file storage, billing, analytics, email systems, logs, and backups. The difficult part is deciding what must disappear, what may be retained, and how to prove that each step completed.

This article describes Homi's current implementation in July 2026. It is an engineering case study, not a claim that one code path makes a product GDPR compliant. Article 17 of the GDPR includes a right to erasure, but that right has conditions and exceptions. The legal result depends on the data, purpose, retention basis, contracts, and operating procedures around the code.

Anonymization is not a label you can apply to a row. If the remaining data can still be linked to a person with information reasonably available to the controller, it may still be personal data.

Start with a data map, not a delete button

For Homi, an account deletion can touch at least five surfaces:

  1. Authentication accounts and active sessions.
  2. The user row and application records that refer to it.
  3. Profile images in object storage.
  4. A customer record in Polar.
  5. Person profiles in PostHog and Amplitude.

Backups, logs, and any later restoration process sit outside the request path, but they still belong in the deletion design. The European Data Protection Board's 2026 enforcement report on the right to erasure specifically calls out ineffective anonymization, unclear retention periods, and deletion from backups as recurring problems.

That changes the engineering question. Instead of asking, "Did the endpoint return 200?", ask:

  • Which systems received personal data?
  • Which records must be deleted, de-identified, or retained under another legal basis?
  • Can a retained identifier still be connected to the person?
  • What happens when one provider is unavailable?
  • What happens if a backup is restored next month?
  • What evidence shows that the request finished?

Block deletion while shared work still needs an owner

Homi collections can have several collaborators. Deleting an owner without a transfer would leave a shared search in an unclear state, so the service checks active ownership first:

const [ownedCollections, createdListings] = await Promise.all([
  db.$count(
    collectionTable,
    and(
      eq(collectionTable.ownerId, userId),
      isNull(collectionTable.deletedAt),
    ),
  ),
  db.$count(
    propertyListingTable,
    and(
      eq(propertyListingTable.creatorId, userId),
      isNull(propertyListingTable.deletedAt),
    ),
  ),
]);

if (ownedCollections > 0) {
  blockers.push(`owns ${ownedCollections} active collections`);
}

Created property listings do not block deletion. They remain attached to the retained internal user ID after identifying fields on the user row are cleared. Active collection ownership does block deletion until the user transfers or removes those collections.

This is a product rule, not a GDPR exception. The interface should explain the blocker and give the user a direct way to resolve it. A blocker without a resolution path turns a privacy control into a support ticket.

Revoke access before changing the profile

Changing an email address does not invalidate OAuth credentials or existing sessions. Homi explicitly removes both:

await db.delete(accountTable).where(eq(accountTable.userId, userId));
await db.delete(sessionTable).where(eq(sessionTable.userId, userId));

This stops an existing session and prevents an OAuth account record from reconnecting the deleted profile. Only then does the service mark the user as deleted and clear identifying fields:

const [anonymizedUser] = await db
  .update(userTable)
  .set({
    deletedAt: new Date(),
    email: `deleted-${userId}@deleted.local`,
    name: null,
    displayName: null,
    image: null,
    phone: null,
    address: null,
    city: null,
    state: null,
    zipCode: null,
    country: null,
    gender: null,
    birthDate: null,
    nationality: null,
  })
  .where(eq(userTable.id, userId))
  .returning();

The internal ID remains because property and collaboration records still refer to it. We describe this as de-identification inside the application, not guaranteed anonymization. Whether the result is truly anonymous requires a separate assessment of linkability, singling out, inference, and the information still available to Homi or another party.

The EDPB published draft Guidelines 02/2026 on Anonymisation in July 2026. The draft is useful context, but it is still open for consultation and should not be treated as final guidance.

Clean up files and billing records

The database is only one system. Homi removes a Vercel Blob profile image when the stored URL points to that service, then asks Polar to delete the customer identified by the Homi user ID.

if (userToDelete.image?.includes("blob.vercel-storage.com")) {
  await BlobService.delete(userToDelete.image);
}

await getPolarClient().customers.deleteExternal({
  externalId: userId,
});

The production code catches failures from both providers so a temporary vendor error does not leave the user signed in. That is a deliberate availability tradeoff, but logging alone is not a complete deletion workflow. A robust implementation should also place failed work on a durable retry queue, record each attempt, alert after repeated failures, and give an operator a way to close the request.

Scrub analytics profiles without relying on consent

Homi sends product analytics to PostHog and Amplitude. Normal identify calls require analytics consent. A deletion scrub is different: it removes identifying traits, so the service deliberately runs it even if the user never granted analytics consent or later withdrew it.

identifyServerUser(userId, anonymizedUser, {
  bypassConsent: true,
});

trackServerEvent(userId, "user_deleted", undefined);

Both analytics providers receive the cleared user properties. Homi also records an operational user_deleted event against the retained internal ID.

This preserves aggregate product history, but it does not prove that every event is anonymous. Event properties may contain free text, URLs, location data, imported listing details, or another identifier. The deletion design therefore needs an inventory of event properties as well as user properties. Provider-side person deletion may be safer when historical events cannot be reliably scrubbed or separated from a person.

There is also a delivery problem. Homi schedules analytics work after the response begins. If that background call fails, the database change can succeed while a provider profile remains unchanged. The next improvement is to turn vendor cleanup into durable, idempotent jobs with visible completion states.

Treat backups as delayed deletion, not an exception you can ignore

Deleting a record from the live database does not rewrite every historical backup immediately. A workable backup policy needs:

  • A documented retention period.
  • Access controls that prevent backups from becoming an alternate production database.
  • A record of deletion requests that can be replayed after a restore.
  • A tested restore procedure that reapplies deletions before normal service resumes.
  • A way to explain the timing to the person making the request.

The exact design depends on the database and backup provider. The important part is that restoration cannot silently bring a deleted profile back into active use.

Use one workflow for self-service and admin deletion

Homi exposes deletion through account settings and an admin surface. Both entry points call the same dependency check and deletion service. That keeps the underlying behavior consistent while allowing each interface to apply its own authorization and confirmation rules.

The shared service handles the current sequence:

  1. Check active collection ownership.
  2. Delete authentication accounts and sessions.
  3. De-identify the application user row.
  4. Remove the profile image when applicable.
  5. Request deletion of the Polar customer.
  6. Scrub PostHog and Amplitude user properties.
  7. Record an operational deletion event.

The endpoint result is only one part of completion. Vendor retries, backup expiry, audit evidence, and retained-data review need their own states.

What we would require before calling the workflow complete

The current path gets the most urgent product behavior right: it revokes access, clears the main profile, protects shared collections, and reaches the services we know receive user traits. It also has clear gaps that are easy to hide behind a green success response.

Our completion checklist is:

  • Every data store and processor has an owner and a documented deletion action.
  • Retained fields have a stated purpose, legal basis, and retention period.
  • De-identification has been tested for linkability, not assumed from null fields.
  • Third-party failures enter a durable retry path.
  • The user can see and resolve ownership blockers.
  • Restores reapply deletion requests before data returns to service.
  • Operators can prove which steps completed without putting deleted personal data into the audit log.

That is the durable lesson from this implementation. Account deletion is a workflow with legal and operational state. The button is the easy part.

Plan a home search together

Use Homi to collect listings, compare tradeoffs, and keep everyone in the search on the same page.

Homi Platform Screenshot

About the Author

kristian

kristian

Engineering at Homi, building the future of real estate technology.

Related Posts

Continue reading with these related articles

Kristian Elset BøKristian Elset Bø

No UI Survives First Contact with Users

How we rebuilt our 'Add Property' dialog three times in one session based on real user feedback. A case study in iterative design and the importance of staying flexible.

#engineering#ui-design#user-feedback#iteration
Kristian Elset BøKristian Elset Bø

Email Audience Segmentation Without Schema Pollution

How we built a campaign-ready sync system for Loops that computes dynamic user segments on-demand without polluting our database schema or scattering one-off updates throughout our codebase.

#engineering#email-marketing#backend#data-architecture

Want our product updates? Sign up for our newsletter.

We care about your data. Read our privacy policy.