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
- Python
- Temporal
- gRPC
- MySQL
- Auth0
- Kong
- AWS SNS
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
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.