Data Deletion

GDPR/CCPA deletion requests arrive from the app, from MX through the backoffice, and from Zendesk "Delete My Data" tickets. Every request is recorded as a Data deletion request row and produces a message for the janitor, which performs the vendor scrub and writes the final status. The janitor also runs the post-cancellation cleanup queued by account closure and bans.

Sources and Scopes

Source Operation Scope Response

app

POST /{user_id}/user/data-deletion/request

full when cancel_membership is true, else marketing

200 {"status": "accepted"}; 404 unknown user

backoffice

POST /{user_id}/user/data-deletion/marketing

marketing

204; 404 unknown user

website

POST /webhooks/zendesk/data-deletion (Zendesk trigger via the edge Cloudflare Worker)

From the ticket tags — see Zendesk Webhook

204; 400 on a malformed request

Scope Effect

marketing

Vendor scrub only (Vendor Signals). The account stays open.

full

The membership is scheduled for cancellation (PENDING_CANCELLATION, event CANCEL, event source = request source) so access lasts to the end of the billing term, plus the marketing scrub now. Plaid removal, the Auth0 block and Array cleanup run when the Subscription Service later calls CancelAccount.

Processing a request with a known user: write the pending row (requested_at = now), schedule the cancellation for full, then queue a marketing cleanup message carrying the user id, email, source and requested_at. Metric user.data_deletion with scope:, source: and user_found: tags.

Zendesk Webhook

  1. Scope from the ticket tags: delete_all_floatme_data → full, else delete_marketing_data → marketing; neither is 400 "no recognizable deletion tag". requester_email is required (400).

  2. Look the requester up by email, then by phone when exactly one user matches.

  3. A ticket already solved or closed is skipped (204).

  4. Dispatch:

Scope Account Action

marketing

found

Process as above; post the marketing-data comment; solve the ticket

marketing

none

Anonymous marketing scrub (below); solve the ticket with the marketing-data comment

full

found

Process as above; solve the ticket with the cancellation comment, naming the billing-cycle end date from the Subscription Service when available

full

none

Anonymous marketing scrub; solve the ticket with the "no account found, resubmit" comment and add the no_account_found tag

An anonymous scrub writes the row under USERS#anon-<sha256(email)[:16]> with scope marketing and queues a cleanup message with an empty user id and the email. Zendesk comment and close failures are logged only; the solved ticket is assigned to the Ollie service account. Comment texts live in pkg/zendesk/deletion_responses.go.

Janitor

The cleanup Lambda consumes {env}-user-service-cleanup. A message that fails processing is reported as a batch item failure and retried; after 5 receives it lands on the DLQ.

Message

Field Meaning

user_id

Empty for anonymous scrubs

scope

full or marketing

email

Present for anonymous scrubs

requested_at

Locates the deletion row to update; zero when no row exists

source

app, website, backoffice, or empty for cancellations and bans

A body that is a bare user_id string (sent by account closure and bans) is treated as {"user_id": …, "scope": "full"}.

Source: pkg/cleanup/cleanup.go

Pipeline

Steps run in order; each failure is collected and the remaining steps still run.

  1. full scope only: remove every active Plaid item in the Transactions Service (a 412 means the user has an active float and the removal stops silently), block the Auth0 account, schedule Array cleanup in the Entitlements Service (201 or 404 counts as success).

  2. Vendor signals, only when source is app, website or backoffice.

  3. When requested_at is set, update the deletion row: status completed, or partial_failure when any step or any vendor call returned an error, completed_at, and vendor_results. A failed status write is logged and does not retry the message.

Vendor Signals

Skipped entirely, with every vendor recorded as skipped: gdpr_signals_disabled, when the GrowthBook flag gdpr_signals_enabled is off (Feature Flags). With a user_id the email and AppsFlyer id come from the user record; without one only the email is available.

Vendor Call Recorded result

Iterable

Forget user by email

ok or error: …

AppsFlyer

Request deletion by appsflyer_id

ok, error: …, skipped: no_appsflyer_id, or skipped: anonymous_mode

Segment

Suppress and delete by user_id

ok, error: …, or skipped: anonymous_mode

Only error: results count towards partial_failure.