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.
A practical account deletion design for auth, application data, analytics, vendors, and backups, with the limits of anonymization made explicit.
kristian
Engineer

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.
For Homi, an account deletion can touch at least five surfaces:
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:
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.
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.
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.
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.
Deleting a record from the live database does not rewrite every historical backup immediately. A workable backup policy needs:
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.
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:
The endpoint result is only one part of completion. Vendor retries, backup expiry, audit evidence, and retained-data review need their own states.
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:
That is the durable lesson from this implementation. Account deletion is a workflow with legal and operational state. The button is the easy part.
Use Homi to collect listings, compare tradeoffs, and keep everyone in the search on the same page.


Engineering at Homi, building the future of real estate technology.
Continue reading with these related articles
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.
How Homi uses OpenAI's GA Realtime API, short-lived client secrets, WebRTC, and a shared tool registry to turn conversation into property-search actions.
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.