Stratify
Legal and compliance

Data Subject Access & Erasure Request Procedure

docs/compliance/isms/procedures/PRO-09-data-subject-access-requests.md

Source updated 03. Aug. 2026

Data Subject Access & Erasure Request Procedure

Documents how stratify actually fulfils a GDPR Art. 15 (access) or Art. 17 (erasure) request today. Both paths are real, working code, not a plan — this procedure describes the tool that exists and the manual process wrapped around it, and states the honest gap plainly rather than implying a self-service flow that doesn't exist yet.

1. What exists today

  • Export (Art. 15). buildGdprExport() (apps/web/lib/admin/gdpr-export.ts:14-42) is a

read-only query across subscribers, user_active_mandate, onboarding_profile, subscriber_roles, web_push_subscriptions, and up to 500 audit_log rows matching the subscriber as actor or subject. It is served via GET /subscribers/[userId]/export (apps/web/app/(admin)/subscribers/[userId]/export/route.ts), gated to the admin, founder, and compliance roles, and downloads as a dated JSON file.

  • Erasure (Art. 17). eraseSubscriberData() (apps/web/app/(admin)/subscribers/actions.ts:91-160),

gated by requireComplianceActor() (admin, founder, or compliance role). It hard-deletes web_push_subscriptions, onboarding_profile, user_active_mandate, and subscriber_roles; anonymizes the subscribers row (email → erased+<id8>@stratify.invalid, name nulled, status → banned, Stripe ID and referral attribution cleared); and optionally hard-deletes the Supabase Auth user. It writes a gdpr.erasure.completed event to audit_log — the erasure itself is logged, deliberately, even as the underlying data is removed.

  • Operator UI. apps/web/app/(admin)/subscribers/subscriber-gdpr-panel.tsx exposes both: a

"Datenexport (JSON)" link and an erasure form requiring a reason of at least 10 characters, a checkbox for whether to also delete the Auth user, and a browser confirm dialog before submit.

2. What this is not

Both actions are triggered by an admin/compliance-role operator working the subscriber detail page — they are not a subscriber-facing /account button. A subscriber cannot today export or delete their own data through the product UI. This matches dsgvo-data-flow.md's own framing of self-service export/erasure as "mid-term" / "Phase B" work. Until that ships, every request is fulfilled manually by a human with one of the three gated roles, using the panel described above.

3. Intake

  1. A data subject sends a request to `[email protected]` (register/facts.md). Antonios

monitors this inbox as DPO / Privacy Owner.

  1. Antonios records the request: date received, requester's email, request type (access, erasure,

or both), and a short description. No dedicated request-tracking system exists yet — until one does, this is a dated entry kept alongside the eventual fulfilment record (see §6).

  1. If the request requires technical execution and Antonios does not hold an operator role with DB

access, he hands the userId/email to Toby (ISMS Owner) to execute via the panel, while Antonios remains accountable for the response to the requester (matches the RACI split in governance/GOV-03-roles-and-raci.md §2: Toby R for technical execution, Antonios A for legal sufficiency and subject communication).

4. Identity verification

There is no dedicated verification tool. The proposed, concrete check:

  1. Search the admin subscriber list for the requester's email. Treat as verified if there is

exactly one subscriber row whose email column exactly matches the address the request was sent from.

  1. If the email doesn't match any row, matches more than one (should not happen given email

uniqueness, but check), or the request looks anomalous in any way (different name than on file, urgency pressure, ambiguous account reference), require a stronger check before acting: ask the requester to complete a fresh sign-in on the address on file using the existing 6-digit email OTP flow (docs/adr/0002-email-code-auth-instead-of-links.md) and confirm the resulting session's user_id matches. This reuses a control that already exists rather than inventing new tooling.

  1. Erasure requests always get the stronger check in step 2, given the action is irreversible.

5. Fulfilment

  • Access. Operator opens /subscribers/[userId], clicks "Datenexport (JSON)", and the export

downloads via buildGdprExport(). Antonios reviews the export contents before forwarding (no automatic redaction is applied — the bundle is a raw dump of the tables above) and sends it to the requester from [email protected].

  • Erasure. Operator opens the same page, enters a reason (ticket reference recommended, ≥10

characters), decides whether to also delete the Supabase Auth user, confirms the browser dialog, and submits. eraseSubscriberData() executes synchronously and returns success/failure inline. Antonios confirms completion to the requester once the panel reports success.

6. Response deadline and logging

  • Respond within GDPR Art. 12(3)'s one-month deadline from intake, extendable by two further months

for complex requests provided the requester is told of the extension within the first month.

  • Erasure is self-logging: the gdpr.erasure.completed audit_log entry is written

automatically as part of eraseSubscriberData().

  • Access is not. Checked directly against the code: the export route

(apps/web/app/(admin)/subscribers/[userId]/export/route.ts) contains no call to recordAudit() — an access/export request leaves zero trace in audit_log today, not merely a non-distinct event type. This is a real, confirmed gap. Until it's closed in code (adding a gdpr.export.completed event mirroring the erasure pattern), Antonios records the fulfilment manually in the request log started at intake (§3): date, requester, userId, operator who ran the export. This manual record is the evidence of record for access requests until the code gap closes.

7. Evidence this procedure produces

  • For erasure: the gdpr.erasure.completed audit_log row (queryable via service-role key).
  • For access: the manual request-log entry described in §6, until the code-level gap is closed.
  • The dated response sent to the requester (kept in the DPO's email record).

8. Known gaps, stated plainly

  • No subscriber self-service export/erasure exists (Phase B per dsgvo-data-flow.md).
  • Access requests are not written to audit_log at all (§6) — propose closing this by adding a

recordAudit() call to the export route, event type gdpr.export.completed, mirroring the erasure pattern exactly.

  • No dedicated request-tracking system exists; the interim log in §3/§6 is a manual substitute.
  • Rectification (Art. 16) and restriction (Art. 18) are not covered by this procedure — they route

through ordinary /account/* self-service or ad hoc admin edits respectively, per dsgvo-data-flow.md §7, and are not backed by dedicated tooling either.

9. Review

Reviewed annually and immediately upon any change to buildGdprExport(), eraseSubscriberData(), or the role gates protecting either.