GDPR Data Subject Requests

A data subject request (DSAR) asks you to find, return, correct, or delete every piece of personal data you hold about one individual — and in a multi-tenant SaaS that individual usually belongs to exactly one tenant, while your platform stores their data across shared tables, per-tenant schemas, object storage, search indexes, queues, logs, and a dozen third-party processors. Resolving a DSAR correctly is therefore a data-mapping problem before it is a code problem, and it sits squarely inside the broader Multi-Tenant Compliance & Data Governance framework that governs how you prove what you did with each tenant's records.

GDPR Articles 15–22 give the subject six concrete rights you must serve: access (Art. 15), rectification (Art. 16), erasure (Art. 17), restriction (Art. 18), portability (Art. 20), and objection (Art. 21). The catch for a SaaS vendor is Article 28: for your tenants' end users you are almost always the processor, not the controller, so you act only on the tenant's documented instruction — except for your own direct customer relationships and your logs, where you are the controller. Getting that distinction wrong is the most common way teams either over-delete (destroying a tenant's records on a request they had no authority to honour) or under-respond (ignoring a request you were legally obligated to forward).

Prerequisites

Confirm all of these before you build a single DSAR handler. A request that arrives before this foundation exists will be answered incompletely, and an incomplete erasure is a reportable breach.

Step-by-Step Implementation

The workflow is six ordered stages: intake and authority check, identity verification, discovery across every store, fulfilment by request type, propagation to processors, and proof. Run them in order — fulfilment before the authority check is how a processor deletes data a controller never asked it to touch.

1. Record the request and resolve controller-vs-processor authority

Every DSAR begins as an immutable ledger row. The first decision is whether the requesting tenant has authority over this subject. If the subject is a tenant's end user, the tenant is the controller and must have instructed you; if the subject is your own account holder, you are the controller and act directly.

CREATE TABLE dsar_requests (
  id            uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id     uuid NOT NULL,
  subject_id    uuid NOT NULL,
  subject_email text NOT NULL,
  request_type  text NOT NULL
                CHECK (request_type IN
                  ('access','rectification','erasure','restriction','portability','objection')),
  our_role      text NOT NULL CHECK (our_role IN ('controller','processor')),
  received_at   timestamptz NOT NULL DEFAULT now(),
  due_at        timestamptz NOT NULL DEFAULT now() + interval '30 days',
  status        text NOT NULL DEFAULT 'received'
                CHECK (status IN ('received','verifying','in_progress','fulfilled','rejected')),
  closed_at     timestamptz
);

2. Verify the subject's identity before disclosing anything

Article 12(6) lets you request additional identification when you have reasonable doubt. Never disclose or erase on an unverified email alone — a forged DSAR is a data-exfiltration vector. Issue a single-use, short-TTL token bound to the request and the tenant.

import { randomBytes, createHash } from "node:crypto";

export async function issueVerificationToken(requestId: string, tenantId: string) {
  const raw = randomBytes(32).toString("base64url");
  const tokenHash = createHash("sha256").update(raw).digest("hex");
  await db.query(
    `INSERT INTO dsar_verifications (request_id, tenant_id, token_hash, expires_at)
     VALUES ($1, $2, $3, now() + interval '24 hours')`,
    [requestId, tenantId, tokenHash],
  );
  // Email `raw` to subject_email only; store only the hash.
  return raw;
}

3. Discover every record across the per-tenant inventory

Discovery is where multi-tenancy hurts. You must enumerate personal data in the store of record, object storage, the search index, and any analytics sink — all filtered to the one tenant_id. Drive discovery from the inventory, not from memory, so a new table added last quarter is not silently missed.

INVENTORY = [
    {"store": "postgres", "table": "users",     "subject_key": "id"},
    {"store": "postgres", "table": "tickets",   "subject_key": "author_id"},
    {"store": "postgres", "table": "audit_log", "subject_key": "actor_id"},
    {"store": "s3",       "prefix": "uploads/{tenant_id}/{subject_id}/"},
    {"store": "opensearch", "index": "messages-{tenant_id}", "subject_key": "author_id"},
]

def discover(tenant_id: str, subject_id: str) -> dict:
    found = {}
    for src in INVENTORY:
        if src["store"] == "postgres":
            rows = pg.fetch(
                f'SELECT * FROM {src["table"]} '
                f'WHERE tenant_id = %s AND {src["subject_key"]} = %s',
                (tenant_id, subject_id))
            found[src["table"]] = rows
        elif src["store"] == "s3":
            prefix = src["prefix"].format(tenant_id=tenant_id, subject_id=subject_id)
            found[prefix] = s3.list_objects(Bucket="tenant-data", Prefix=prefix)
    return found

4. Fulfil access, export, and portability

Access (Art. 15) returns a human-readable copy; portability (Art. 20) returns a structured, machine-readable export of data the subject provided, in a commonly used format such as JSON. Build the export from the discovery result, redact other subjects' data that appears in shared rows, and never include another tenant's records even if a join touched them.

import json

def build_export(tenant_id: str, subject_id: str) -> bytes:
    data = discover(tenant_id, subject_id)
    package = {
        "subject_id": subject_id,
        "tenant_id": tenant_id,
        "generated_at": datetime.utcnow().isoformat() + "Z",
        "format": "GDPR-Art20-portability/1.0",
        "records": {k: redact_other_subjects(v, subject_id) for k, v in data.items()},
    }
    return json.dumps(package, indent=2, default=str).encode("utf-8")

5. Fulfil rectification and erasure within the tenant boundary

Rectification (Art. 16) updates inaccurate fields. Erasure (Art. 17) — the right to be forgotten — is the dangerous one: it must remove or irreversibly anonymise the subject's data without breaking referential integrity or deleting another tenant's rows. For records you must retain for a legal basis (invoices, fraud logs), anonymise the identifying fields instead of deleting the row. The mechanics of cascading this safely are detailed in per-tenant data deletion workflows.

-- Anonymise where retention law requires the row to survive; tenant-scoped, always.
UPDATE invoices
SET billing_name  = 'REDACTED',
    billing_email = concat('erased+', id, '@invalid.example'),
    erased_at     = now()
WHERE tenant_id = $1 AND customer_id = $2;

-- Hard-delete where no retention basis applies.
DELETE FROM tickets        WHERE tenant_id = $1 AND author_id = $2;
DELETE FROM message_search WHERE tenant_id = $1 AND author_id = $2;

6. Propagate to sub-processors and record proof

Erasure and rectification must reach every sub-processor that received the data (Art. 19). Fan out the deletion to each external API, capture each acknowledgement, and write the result to the ledger. The ledger row is your evidence that you met the request — it must outlive the data it describes.

async function propagateErasure(req: DsarRequest) {
  const processors = [
    () => stripe.customers.del(req.externalIds.stripe),
    () => emailVendor.suppress(req.subjectEmail),
    () => analytics.deleteUser({ tenantId: req.tenantId, userId: req.subjectId }),
  ];
  for (const call of processors) {
    const ack = await call().catch((e) => ({ error: String(e) }));
    await db.query(
      `INSERT INTO dsar_processor_log (request_id, processor, ack, acked_at)
       VALUES ($1, $2, $3, now())`,
      [req.id, call.name, JSON.stringify(ack)],
    );
  }
  await db.query(
    `UPDATE dsar_requests SET status='fulfilled', closed_at=now() WHERE id=$1`,
    [req.id],
  );
}

The end-to-end path — from an intake that resolves authority, through tenant-scoped discovery, to fan-out and a permanent ledger entry — is what keeps a DSAR both complete and inside the right tenant boundary.

Choosing how to satisfy each right

Different rights demand different mechanics, and the right one depends on whether a retention obligation competes with the erasure duty.

Request type Primary action Multi-tenant constraint Retention conflict handling
Access (Art. 15) Read + render copy Filter every query by tenant_id None — disclosure only
Portability (Art. 20) Structured JSON export Exclude data the subject did not provide None
Rectification (Art. 16) Update fields, re-index Update store of record + search index together None
Erasure (Art. 17) Delete or anonymise Cascade within tenant only; touch no other tenant Anonymise rows under legal hold
Restriction (Art. 18) Flag, block processing Suppress from jobs and exports, do not delete Row kept, processing frozen
Objection (Art. 21) Stop a processing purpose Disable the specific pipeline for the subject Other lawful purposes continue

Dynamic Query Scoping & Connection Handling

Every DSAR query touches personal data, so every one of them must run inside the tenant boundary, never as an unscoped admin sweep. The discovery and fulfilment steps should execute under the same tenant context your application uses for normal traffic — the same session-variable injection and connection-pool discipline described across the routing layer — so that a missing tenant_id fails closed rather than scanning the whole table. Set the tenant on the connection, run the DSAR transaction, and reset it on return to the pool.

-- Run DSAR work inside the tenant's row-security context, not as a superuser.
SET LOCAL app.tenant_id = '8f3a...';
SET LOCAL app.dsar_request = 'r-2026-0042';   -- so audit triggers attribute the change
-- ... discovery / erasure statements execute here, all policy-checked ...

Run erasure inside a single transaction per store so a partial delete cannot leave the subject half-removed. For object storage and external APIs that are not transactional, make every call idempotent and retry-safe, and treat the ledger — not the remote system — as the source of truth for what has been confirmed.

Security Enforcement & Access Control

DSAR tooling is high-privilege: it reads and destroys personal data across stores. Treat the DSAR operator as a distinct role with its own audit trail, separate from ordinary support staff and from the application's runtime role.

Access layer Who acts Enforcement Audit record
Request intake Tenant admin or subject DPA authority check, signed request dsar_requests row
Identity proof Subject Single-use 24h hashed token dsar_verifications row
Discovery/export DSAR operator role Tenant-scoped reads only Per-store query log
Erasure execution DSAR operator + approver Dual control, SET LOCAL tenant context dsar_processor_log + DB audit
Ledger review Compliance/DPO Read-only, append-only ledger Immutable

The operator never operates outside a tenant context, dual control gates irreversible erasure, and the request ledger is append-only so the act of deleting data cannot delete the record that you deleted it.

Operational Overhead & Scaling Metrics

DSAR volume scales with your end-user count, not your tenant count, so a few large tenants can dominate. Track these to keep the 30-day clock from slipping.

Metric Threshold Mitigation
Time-to-fulfil (p95) < 21 days (buffer before the 30-day legal limit) Automate discovery; alert at day 14
Discovery coverage 100% of inventory sources Fail the request if any source is unreachable
Processor ack rate 100% within processor SLA Escalate unacknowledged deletions to the DPO
Erasure verification rate 100% re-checked after delete Post-delete confirmation query per store
Open requests past day 25 0 Page on-call; trigger the documented 60-day extension only with justification

Pitfalls & Anti-Patterns

Treating yourself as controller for tenant data. Honouring an erasure request from a random end user without the tenant's instruction destroys records the tenant — the actual controller — was obligated to keep. Always resolve our_role first; for tenant end users you act only on the tenant's documented instruction under the DPA.

Forgetting the shadow stores. Teams delete from the primary database and declare victory while the subject's data lives on in the search index, the read-replica's lag window, last night's backup, the message queue, and the analytics warehouse. Drive every erasure from the inventory and document a backup-expiry policy so restores cannot resurrect erased subjects.

Cross-tenant leakage in exports. A naive join to enrich an export can pull in rows owned by another tenant, or another subject's data embedded in a shared conversation. Every discovery query must carry tenant_id, and exports must redact other subjects before they leave the system.

Hard-deleting data under legal hold. Erasure does not override a competing retention obligation such as tax-record or anti-fraud law. Deleting an invoice to satisfy Art. 17 can breach a different statute; anonymise the identifying fields and keep the row instead.

Letting the ledger be erasable. If your "delete everything about this person" routine also deletes their DSAR record, you lose the only proof that you complied. Keep the request ledger and processor acknowledgements in an append-only store that the erasure path cannot touch.

Frequently Asked Questions

Are we the controller or the processor for a DSAR? For your tenants' end users you are almost always the processor under Article 28, acting only on the tenant's documented instruction — the tenant is the controller. For your own direct customers and your operational logs you are the controller. Resolve this per request before doing anything, because it determines whether you fulfil, forward, or reject.

How long do we have to respond? One month from receipt under Article 12(3), extendable by a further two months for complex or numerous requests if you inform the subject of the extension and the reason within the first month. Treat 21 days as your internal target so verification delays do not push you past the legal limit.

Do we have to delete data we are legally required to keep? No. Article 17(3) exempts data you must retain for a legal obligation or to establish a legal claim. Anonymise the identifying fields so the row no longer relates to an identifiable person, keep the record for the retention period, and document why it was retained rather than erased.

Does erasure have to reach our sub-processors? Yes. Article 19 requires you to communicate erasure and rectification to every recipient the data was disclosed to, unless it is impossible or disproportionate. Maintain a processor register, fan the request out to each one's deletion API, and record every acknowledgement in your ledger.

What about backups and replicas? You cannot always purge a subject from an immutable backup immediately. The accepted approach is a documented backup-retention window plus a rule that any restore re-applies outstanding erasures, so erased subjects never silently reappear after a recovery.