Customer data export (GDPR)
GDPR gives EU data subjects two rights we care about operationally:
- Art. 15 — Right of access: give the subject a copy of the personal data we hold about them, in a portable format.
- Art. 17 — Right to erasure ("right to be forgotten"): delete the personal data, with documented exceptions for legal-hold, billing history, and audit-trail retention required by the DPA.
Statutory deadline: 30 days from request receipt. We aim for 10 business days as the internal SLA.
Request intake
Section titled “Request intake”1. Acknowledge within 72 hours
Section titled “1. Acknowledge within 72 hours”Reply from privacy@tappass.ai confirming receipt. Template:
Hi <name>,
We've received your request under GDPR Art. <15 / 17> regarding theaccount associated with <email>.
We'll complete the request within 30 days. Before we can proceed weneed to verify your identity — please reply to this email from theaddress associated with your TapPass account, and include theworkspace URL you normally log in to.
— TapPass Privacy2. Identity verification
Section titled “2. Identity verification”Acceptable verification evidence (pick one):
- Reply from the account email.
- Signed-in session artefact (user pastes the final segment of a
recent
tp_*dev key they hold — we verify hash-match in our DB). - For enterprise contracts: a DPO-signed request using the contact on file.
Do not accept identity proof from a different email domain or over chat without a verified identity anchor.
Scope the data
Section titled “Scope the data”The personal data TapPass holds for a user, by table:
| Table | Personal data | Export? | Erase? |
|---|---|---|---|
accounts | email, name, org_id | yes | yes (Art. 17) |
auth_sessions | JWT refresh token hashes | yes | yes |
developer_keys | hashed API keys linked to user | yes | yes |
audit_events | user_id, request bodies in details | yes | no — 7-year retention under DPA |
invites | email if user was ever invited | yes | yes |
org_members | role + timestamps | yes | yes |
feedback, unsubscribe | email, message body | yes | yes |
The audit-trail retention exception is explicit in the DPA with every customer. We document this in the export.
Do a dry-run query against staging if the user's shape is unusual (enterprise with many agents, historic account, deleted-then- reactivated).
Export (Art. 15)
Section titled “Export (Art. 15)”1. Connect to prod read-replica
Section titled “1. Connect to prod read-replica”# Use the read-only user — never run an export against the primarygcloud sql connect tappass-db --project=tappass-prod \ --user=tappass-readonly2. Run the canonical export query
Section titled “2. Run the canonical export query”-- Replace :email\set target_email '''jane@example.com'''
-- 1. AccountSELECT id, email, full_name, org_id, created_at, last_login_atFROM accounts WHERE email = :target_email;
-- 2. Sessions (metadata only — hashes not reversible)SELECT id, created_at, last_seen_at, ip_address_maskedFROM auth_sessions sJOIN accounts a ON s.account_id = a.idWHERE a.email = :target_email;
-- 3. Developer keys (hashes only)SELECT key_id, name, created_at, last_used, revokedFROM developer_keys dkJOIN accounts a ON dk.developer_email = a.emailWHERE a.email = :target_email;
-- 4. Audit events (the biggest payload)SELECT id, org_id, event_type, agent_id, task_id, details, created_atFROM audit_events aeJOIN accounts a ON ae.user_id = a.emailWHERE a.email = :target_emailORDER BY created_at;
-- 5. Invites + org membershipsSELECT * FROM invites WHERE email = :target_email;SELECT * FROM org_members omJOIN accounts a ON om.account_id = a.idWHERE a.email = :target_email;
-- 6. Feedback + unsubscribeSELECT * FROM feedback WHERE email = :target_email;SELECT * FROM unsubscribe WHERE email = :target_email;3. Package the export
Section titled “3. Package the export”# Dump each result to CSVfor t in accounts sessions developer_keys audit_events invites org_members feedback; do psql "…" -c "COPY (<query from above>) TO STDOUT WITH CSV HEADER" \ > export/$t.csvdone
# Bundle with README explaining column semanticscd exportecho "# TapPass data export for <email>" > README.mdecho "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> README.mdecho "Under GDPR Art. 15 request #<ticket>" >> README.mdzip -r export-$(date +%Y%m%d)-<ticket>.zip .4. Deliver
Section titled “4. Deliver”Upload to a time-limited GCS signed URL (7-day expiry):
gsutil cp export-YYYYMMDD-<ticket>.zip gs://tappass-prod-privacy-exports/gsutil signurl -d 7d <path-to-sa-key>.json \ gs://tappass-prod-privacy-exports/export-YYYYMMDD-<ticket>.zipReply from privacy@tappass.ai with the signed URL and a one-line
"what's inside" summary. Never email the ZIP directly — email
isn't a safe channel for PII payloads.
Erasure (Art. 17)
Section titled “Erasure (Art. 17)”Erasure runs after export (so the subject has their data if they want it).
1. Revoke active access
Section titled “1. Revoke active access”-- Immediately kill sessions and keys — stops further activityUPDATE auth_sessions SET revoked_at = NOW() WHERE account_id IN (SELECT id FROM accounts WHERE email = :target_email);
UPDATE developer_keys SET revoked = TRUE, revoked_at = NOW() WHERE developer_email = :target_email;2. Delete personal data
Section titled “2. Delete personal data”The core server has a sanctioned delete path for this — use it rather than hand-rolled SQL. It cascades correctly and emits the audit events required by the DPA.
# Admin-only endpoint, requires SUPER_ADMIN rolecurl -X POST https://eu.tappass.ai/api/admin/accounts/<account-id>/erase \ -H "Authorization: Bearer $TAPPASS_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"ticket_id": "<ticket>", "requested_by_email": "<email>"}'Under the hood this:
- Zeros PII columns on
accounts(email →erased-<uuid>@tappass.ai, full_name →[erased], avatar_url → NULL). - Deletes
auth_sessions,developer_keys,invites,org_members,feedback,unsubscriberows for the subject. - Records an audit event with
event_type=account_erased— preserved under the audit-retention exception.
3. Anonymise audit-trail references
Section titled “3. Anonymise audit-trail references”Audit events are retained 7 years for compliance, but the user_id
field is replaced with the anonymised pseudonym so the events are
still analysable without being personally identifiable:
UPDATE audit_events SET user_id = 'erased-' || id WHERE user_id = :target_email;4. Secondary system sweep
Section titled “4. Secondary system sweep”External systems the user might appear in:
| System | Action |
|---|---|
| Sentry (prod + staging) | Dashboard → User Feedback → search email → delete; also check any error event's user.email and scrub |
| PostHog (EU) | Person profile → delete — the $identify trail is retained as anonymous events |
| Resend | Suppress the email from future sends (list → add to suppressions) |
| Cloudflare | No action required — no PII stored beyond log-retention window (auto-expires) |
| 1Password | If they had a shared vault item (rare), remove membership |
5. Confirm completion
Section titled “5. Confirm completion”Reply to the subject from privacy@tappass.ai:
Hi <name>,
Your GDPR Art. 17 request has been completed. We have:
1. Exported the data we held about you (sent <date> via signed link).2. Deleted your account, sessions, API keys, invites, feedback, and unsubscribe records.3. Replaced your email in historic audit events with an anonymised pseudonym.4. Removed your profile from Sentry, PostHog, and Resend.
As documented in our DPA, we retain the following under theaudit-trail exception (anonymised, not personally identifiable):
- Policy decisions (which pipeline step fired which outcome)- Security events (sign-in attempts, credential access)
This retention is bound by our 7-year cold-storage policy.
If you have questions please reply to this email within 30 days,after which this ticket is closed for our internal records.
— TapPass PrivacyWhere each step lives in code
Section titled “Where each step lives in code”tappass/identity/facade.py:delete_account— account erasure (step 2 of Art. 17).tappass/protocols.py:CredentialStore.delete_user_credentials— vault cleanup.tappass/audit/retention/— audit-event retention policy.tappass/verified/profile.py— advertisederasure_capability: Truein the public Verified profile; don't regress this flag without legal review.
Record keeping
Section titled “Record keeping”Every request, regardless of outcome, is logged to the #privacy
Slack channel and mirrored into the Google Drive folder
TapPass / Privacy / GDPR Requests / YYYY. Keep:
- Initial request email (or forwarded ticket).
- Identity verification evidence.
psqloutput screenshots /.csvdumps hash.- Final completion reply.
Retention: 6 years per our supervisory-authority guidance.
Also see
Section titled “Also see”- Restore from backup — if erasure somehow damaged data needed for billing or ongoing investigations.
- Security → Audit trail internals — what's retained versus erased.
- Security → Compliance program — DPA/DPIA/subprocessor list.