← All work

2025 · Designed and built; owned it through two versions

GDPR-compliant user deactivation with Temporal

A Temporal workflow that removes a user safely and in line with GDPR erasure — checks first, hands over what they own, then anonymises and deletes their data.

Published · Updated

6
systems touched per deactivation
4
ownership checks before any change
1
run per user at a time

Context

Removing a user from a platform sounds like one UPDATE. On Grepsr’s platform it meant changing six systems: the user record, Auth0 login, the Kong API gateway, project ownership, digest subscriptions and delivery destinations. Some of these belong to other services.

If any of them is missed, something breaks quietly. A project with no owner, a digest emailing nobody, an API key that still works for someone who has left.

I built the deactivation as a Temporal workflow behind one RPC, then rebuilt it a year later as requirements grew.

Constraints

  • No shared transaction. The steps span a database, two external identity systems and four internal services.
  • Nothing may be orphaned. Whatever the user owns — projects, digests, the organisation account, services — must go to someone else.
  • GDPR erasure. A departed user’s personal data must be erased, not just hidden behind an inactive flag.
  • Keep history. Runs, reports and activity logs still point at the user, so the row stays and the personal data goes.
  • Not every user is complete. An invited user who never signed up has no Auth0 login or gateway consumer.

Design

User deactivation workflow An admin calls deactivate user. The RPC validates the request, then starts a workflow with an id derived from the user, so only one deactivation runs per user. The workflow first checks what the user owns and fails before any change if a transfer target is missing. It then anonymises the user record, transfers projects, digests, the organisation account, services and delivery destinations to the chosen users, removes the Auth0 login and the gateway consumer, cleans up data and subscriptions, and emits an audit event. Admin deactivate_user RPC one run per user workflow · each step retried 1 · check ownership — fail before any change 2 · anonymise the user record 3 · transfer projects · digests · account · services 4 · remove Auth0 login · gateway consumer 5 · clean up data · subscriptions · audit event
Every check that can fail runs before the first destructive step.

An admin calls deactivate_user with the user and, optionally, who should receive their projects, digests and account ownership. The RPC validates the request synchronously — no self-deactivation, and transfer targets must be in the same organisation — then runs the workflow and returns its result.

Key decisions

Check everything before changing anything. The workflow first asks what the user owns: projects, digests (owned or shared to them), organisation accounts, services. If they own something and the request names no one to receive it, the workflow stops before its first write. The common failure — “you forgot to pick a new owner” — never leaves a half-deactivated user.

Anonymise the row, delete the rest. This is what makes deactivation GDPR-compliant erasure. The user’s name and email are replaced with placeholders, and the status becomes inactive, so foreign keys from runs, reports and logs stay valid. Everything else personal tied to the account is deleted: profile, settings, roles, team and organisation memberships, pending invitations, subscriptions, the Auth0 login and the gateway consumer.

Other services own their data. The workflow does not reach into other databases. Each transfer is a call to the service that owns the data: projects, digests, delivery destinations, account ownership. I added the digest-transfer RPC to the notification service for this.

One deactivation per user. The workflow id is derived from the user, with a reuse policy that only allows a new run after a failed one. A double-click or a retrying client gets “already in progress”, never a second run.

Skip what does not exist. Invited users never signed up, so the workflow skips their Auth0 and gateway steps instead of failing on them.

Failure handling

  • Gateway cleanup must not block deactivation. The first version looked up the user’s gateway consumer by id and failed if the id was missing. For some users it was. Gateway removal is now best-effort, with a logged skip, because a missing consumer is already the desired end state.
  • All logins, not one kind. Auth0 deletion first ran only under some conditions, so some users — social-login users among them — kept their Auth0 accounts. I made the deletion apply to every user being deactivated.
  • A transfer created a self-share. If the new owner already had one of the user’s digests shared with them, the transfer left them sharing a digest with themselves. The transfer now removes those redundant shares.
  • Requirements grew, so the workflow did. Service ownership and delivery destinations were added after launch. A year in, I wrote a second version of the workflow with the ownership checks as explicit steps. It replaced the first behind the same RPC.

What I would add

Each step retries, but the workflow does not undo earlier steps if a later one fails for good. The checks up front catch the common failures. Compensation for the rare late failure is the next step.

Result

Deactivating a user is one call. It either stops early with a reason the admin can fix, or it hands over everything the user owned, removes their access everywhere and erases their personal data, with an audit event at the end.