Skip to content

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.

Reply from privacy@tappass.ai confirming receipt. Template:

Hi <name>,
We've received your request under GDPR Art. <15 / 17> regarding the
account associated with <email>.
We'll complete the request within 30 days. Before we can proceed we
need to verify your identity — please reply to this email from the
address associated with your TapPass account, and include the
workspace URL you normally log in to.
— TapPass Privacy

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.

The personal data TapPass holds for a user, by table:

TablePersonal dataExport?Erase?
accountsemail, name, org_idyesyes (Art. 17)
auth_sessionsJWT refresh token hashesyesyes
developer_keyshashed API keys linked to useryesyes
audit_eventsuser_id, request bodies in detailsyesno — 7-year retention under DPA
invitesemail if user was ever invitedyesyes
org_membersrole + timestampsyesyes
feedback, unsubscribeemail, message bodyyesyes

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).

Terminal window
# Use the read-only user — never run an export against the primary
gcloud sql connect tappass-db --project=tappass-prod \
--user=tappass-readonly
-- Replace :email
\set target_email '''jane@example.com'''
-- 1. Account
SELECT id, email, full_name, org_id, created_at, last_login_at
FROM accounts WHERE email = :target_email;
-- 2. Sessions (metadata only — hashes not reversible)
SELECT id, created_at, last_seen_at, ip_address_masked
FROM auth_sessions s
JOIN accounts a ON s.account_id = a.id
WHERE a.email = :target_email;
-- 3. Developer keys (hashes only)
SELECT key_id, name, created_at, last_used, revoked
FROM developer_keys dk
JOIN accounts a ON dk.developer_email = a.email
WHERE a.email = :target_email;
-- 4. Audit events (the biggest payload)
SELECT id, org_id, event_type, agent_id, task_id, details, created_at
FROM audit_events ae
JOIN accounts a ON ae.user_id = a.email
WHERE a.email = :target_email
ORDER BY created_at;
-- 5. Invites + org memberships
SELECT * FROM invites WHERE email = :target_email;
SELECT * FROM org_members om
JOIN accounts a ON om.account_id = a.id
WHERE a.email = :target_email;
-- 6. Feedback + unsubscribe
SELECT * FROM feedback WHERE email = :target_email;
SELECT * FROM unsubscribe WHERE email = :target_email;
Terminal window
# Dump each result to CSV
for 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.csv
done
# Bundle with README explaining column semantics
cd export
echo "# TapPass data export for <email>" > README.md
echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> README.md
echo "Under GDPR Art. 15 request #<ticket>" >> README.md
zip -r export-$(date +%Y%m%d)-<ticket>.zip .

Upload to a time-limited GCS signed URL (7-day expiry):

Terminal window
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>.zip

Reply 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 runs after export (so the subject has their data if they want it).

-- Immediately kill sessions and keys — stops further activity
UPDATE 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;

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.

Terminal window
# Admin-only endpoint, requires SUPER_ADMIN role
curl -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:

  1. Zeros PII columns on accounts (email → erased-<uuid>@tappass.ai, full_name → [erased], avatar_url → NULL).
  2. Deletes auth_sessions, developer_keys, invites, org_members, feedback, unsubscribe rows for the subject.
  3. Records an audit event with event_type=account_erased — preserved under the audit-retention exception.

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;

External systems the user might appear in:

SystemAction
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
ResendSuppress the email from future sends (list → add to suppressions)
CloudflareNo action required — no PII stored beyond log-retention window (auto-expires)
1PasswordIf they had a shared vault item (rare), remove membership

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 the
audit-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 Privacy
  • 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 — advertised erasure_capability: True in the public Verified profile; don't regress this flag without legal review.

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.
  • psql output screenshots / .csv dumps hash.
  • Final completion reply.

Retention: 6 years per our supervisory-authority guidance.