Crossdeck Docs
Dashboard

Delete a user's data

Privacy & compliance Read time: ~9 min · GDPR / CCPA erasure — forget()

When one of your users exercises their right to erasure — GDPR Article 17, CCPA deletion, or simply "please delete my account" — you forward that request to Crossdeck with a single forget() call. Crossdeck immediately marks the customer record as forgotten, records a hard 30-day deletion deadline, writes a tamper-evident audit entry, and queues the deletion of the customer's stored data — all in one atomic transaction, so none of those four things can happen without the others. This page describes exactly what is deleted, what is retained and why, who is allowed to request erasure on whose behalf, and what you can show an auditor afterwards.

TL;DR

What erasure covers

A forget() request targets one customer — the canonical Crossdeck customer record (cdcust_…) that your identity hint resolves to. On acceptance, Crossdeck does three things immediately and atomically:

  1. Marks the customer record. The customer document is stamped forgottenAt (the request time) and forgetDeadlineAt (30 days later).
  2. Writes the audit entry. An erasure_executed entry is appended to the identity journal — the same append-only, hash-chained log that records every identity decision Crossdeck makes. See The audit trail.
  3. Queues the deletion. A durable deletion-queue row is written, carrying the resolved customer ID, the original identity hints, the request time, and the deadline. The stored data deletion — event rows, identity audit rows, entitlement rows — is processed from this queue asynchronously, because deleting a customer's history can touch thousands of rows and a synchronous HTTP request is not the place to coordinate that.

Client-side, the Web and iOS SDKs additionally wipe everything they hold on the device the moment forget() is called: the stored identity, entitlement cache, the offline event queue, super-properties, and persistent stores. After the call, the SDK is in the same state as a fresh install — the next session mints a brand-new anonymous ID and starts a new entry in the identity graph.

Warehouse retention is a separate, additional backstop.

Independently of erasure requests, Crossdeck's analytics warehouse enforces a global retention window: event data older than 24 months is dropped wholesale, by monthly partition, on a scheduled monthly sweep. This is a storage-wide ceiling on how long any event data can exist — it runs regardless of whether anyone asked to be forgotten.

Financial records are retained — stated plainly

Erasure does not delete financial transaction records. Records of purchases, subscriptions, renewals, and refunds that originate from your payment rails — Stripe, Apple, Google — survive a forget() request.

Why. Legal and accounting retention obligations for financial transactions (tax law, bookkeeping statutes, audit requirements) generally supersede erasure rights — GDPR Article 17(3)(b) itself carves out processing required for compliance with a legal obligation. A payment that actually happened is a fact your books, and your payment processor's books, are required to reflect for years. Crossdeck mirrors that reality rather than pretending otherwise: the behavioural and identity data goes, the financial record of the transaction stays.

Tell your users this before they ask.

If you paste this page to your DPO, this is the paragraph they will care about most. Your own privacy policy should state the same exception: erasure removes analytics, profile, and entitlement data; transaction records are retained for the legally required period. Crossdeck also retains the erasure audit entry itself — see below — because the record that you deleted someone's data is the evidence that you complied.

How to request erasure

All four surfaces hit the same endpoint: POST /v1/identity/forget.

Web

await Crossdeck.forget();

Sends the strongest identity hint the SDK holds (customer ID, else user ID, else anonymous ID) to schedule the server-side deletion, then wipes all local state — identity, entitlements, queue, super-properties, persistent stores. The local wipe runs even if the server call fails: the person just asked to be forgotten, and refusing to clear their device because of a network hiccup would be the wrong call. The failure is recorded in the SDK's debug stream so you can follow up server-side.

iOS / Swift

try await cd.forget()  // cd is your started Crossdeck instance

Same contract: POSTs every identity hint the SDK holds, then always runs the local cleanup — identity, entitlements, super-properties, breadcrumbs — regardless of the server outcome. Outright network failure throws so you can show "couldn't reach Crossdeck — try again" UI, but the local wipe completes even on the throw path.

Node (your backend)

const result = await crossdeck.forget({ userId: "user_847" });
// result = { object: "forgot", crossdeckCustomerId, queuedAt, env }

The hints object accepts any of customerId, userId, anonymousId. Because the Node SDK authenticates with your secret key, it carries full erasure authority — this is the recommended path for erasing identified users (see the authority model).

REST

curl https://api.cross-deck.com/v1/identity/forget \
  -H "Authorization: Bearer cd_sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"userId": "user_847"}'
// 200
{
  "object": "forgot",
  "crossdeckCustomerId": "cdcust_…",
  "queuedAt": 1765432100000,
  "env": "production"
}

Authenticate with Authorization: Bearer <key> or a Crossdeck-Api-Key header. The body requires at least one of customerId, userId, anonymousId; an optional idToken carries the identity proof when you call with a publishable key. crossdeckCustomerId in the response is the customer the server resolved and marked — it is null when no known customer matched the hints (still a 200; see Idempotency).

Client SDK + identified user: pair it with a backend call.

The Web and iOS SDKs run on your publishable key and do not currently attach an ID token to forget(). If the SDK holds an identified user, the server refuses the server-side erasure with 401 identity_token_invalid — by design, see below. The device-local wipe still completes. To finish the job for identified users, call forget() from your backend with your secret key (Node SDK or REST) as part of your account-deletion flow.

Who may erase what

Erasure is destructive, so the authority bar scales with what is being erased:

RequestRequired proofWhy
Erase an identified customer (customerId or userId hint) A secret key (cd_sk_*) — or a publishable key plus a verified ID token whose sub equals the userId A publishable key ships in your client and is public by definition. Without a proof requirement, anyone who knows (or guesses) a user ID could destroy that user's data. The secret key proves the request comes from your backend, which has the user's explicit consent; the ID token proves it comes from the actual signed-in user.
Erase an anonymous-only session (anonymousId hint, no identified customer attached) None beyond a valid API key Anyone holding the anonymous session is that session — there is no stronger proof available, and no identified person to impersonate. The audit entry honestly records the weaker provenance as self_asserted.

The ID-token path accepts a JWT from an auth provider your project has explicitly registered (Firebase Auth today; the issuer must be configured on your project — Crossdeck never trusts an unregistered issuer). The token's signature is verified against the provider's published keys, its audience and expiry are checked, and its sub claim must equal the userId being erased. Only a complete match counts. See Identity verification for how to register an issuer.

A publishable-key request that targets an identified customer without a valid token is rejected with 401 identity_token_invalid and the message tells you both remedies: send an ID token signed for this user, or call from your backend with the secret key.

Timeline — what happens, when

Immediately, in one transaction

When the request is accepted, a single atomic transaction:

All three land together or none at all. If the transaction fails you get a 500 with "retry the call" — the request was not partially recorded.

The 30-day deadline

GDPR requires erasure "without undue delay and in any event within one month". Crossdeck encodes that as a hard 30-day SLA, computed once at request time and written down in two places — the customer record and the queue row. It never moves. Because every queue row carries deadlineAt next to processedAt, an overdue request — deadline passed, processedAt still null — is mechanically identifiable from the data itself, and Crossdeck support can produce the exact queue row and its timestamps for any request you need to account for.

Asynchronous deletion

forget() returns as soon as the request is durably recorded, not when the deletion finishes — erasing a customer's history can touch thousands of rows across storage systems. The queued work covers the customer's stored event rows, identity audit rows, and entitlement rows. Two categories are explicitly outside the deletion's scope:

If the hints don't match anyone

If identity resolution fails at request time — say the user was never identified to Crossdeck, or resolution itself errored — the request is not dropped. The queue row is still written, keyed on whichever hints you sent, and resolution is re-attempted at processing time. An erasure request never silently evaporates because the lookup was unlucky on the first try.

Idempotency

forget() is safe to call more than once for the same person:

The audit trail

Every erasure is recorded in the identity journal — Crossdeck's append-only audit log for every identity decision. The journal's properties are what make the erasure record usable as compliance evidence:

The erasure_executed entry specifically records:

FieldWhat it captures
decisionerasure_executed
inputs.hintsThe identifiers the request supplied (user ID and/or anonymous ID). The audit entry retains these — they are the legal record of whose data was erased.
outputsThe resolved customer ID (or "(unresolved)") and the fields stamped on the record (forgottenAt, forgetDeadlineAt).
evidenceThe authority: a verified ID token (issuer + subject), internal_admin for a secret-key call from your backend, or self_asserted for an anonymous-only erasure.
callerThe calling surface (sdk:v1/identity/forget), the request's IP address, and user agent.
timestampMs, sequenceNumber, entryHash, previousHashThe when, the position in the chain, and the tamper-evidence.

If a regulator or your DPO asks "prove this user was erased, when, and on whose authority", this entry — plus the chain verification around it — is the answer. It is what Crossdeck support will show you for any erasure dispute.

What the dashboard shows after erasure

On the customer's detail page, the Audit timeline — the dashboard's rendering of the identity journal — shows the erasure as "Personal data erased (GDPR)", alongside the evidence kind behind it, in sequence with every other identity decision ever made about that customer. The timeline is read from the same journal described above, so what you see in the dashboard is the audit record, not a separate summary of it.