Webhook icon
Labels icon
If icon
Exit icon
Set icon
Query icon
OutputValues icon
Parallel icon
Request icon
List icon
Switch icon
Downloads icon
Write icon
ArchiveCompress icon
SlackIncomingWebhook icon
Pause icon
Queries icon
DeleteList icon
Fail icon

GDPR Data Subject Request Orchestrator for Access and Erasure

Answer GDPR access and erasure requests with Kestra. Find data in Postgres, S3 and OpenSearch, export a ZIP, or erase after approval with proof.

Categories
BusinessData

Under GDPR, a company has 30 days to answer a data subject's request to see their data (Article 15) or delete it (Article 17). The data is rarely in one place: the customer record lives in the application database, uploads sit in object storage, and support conversations sit in a search index. Teams usually answer by hand, system by system, with no proof of what was done. This blueprint handles both request types end to end. It's for privacy, legal and engineering teams at SaaS companies who receive these requests and need them answered on time, consistently, and with an audit trail.

How it works

  1. A privacy form or helpdesk POSTs the request to the intake webhook, or someone runs the flow manually. The webhook maps the JSON body onto typed inputs, so an invalid email is rejected with HTTP 422 before anything runs.

  2. label_request tags the execution with the request ID and type, and skip_if_done exits if the request already completed, so retried or duplicate submissions do nothing.

  3. register opens an audit record in the KV store (dsar-<request_id>) with the receipt time and the legal deadline, 30 days later.

  4. find_customer looks up the customer ID, and remember_customer stores it under dsar-<request_id>-customer. A re-run after a partial erasure can then still find the person's files even though their email was already anonymized. Then discover (io.kestra.plugin.core.flow.Parallel) searches every system at once:

    • Postgres: the customer row, orders and marketing consents, as one JSON document.
    • OpenSearch: support tickets matching the email.
    • S3-compatible storage: the files under customers/<customer id>/.

    The email is always bound as a query parameter, never written into the SQL.

  5. by_type (io.kestra.plugin.core.flow.Switch) branches on the request type:

    • ACCESS: downloads the files, writes the database records and tickets as JSON, packages everything into one ZIP (io.kestra.plugin.compress.ArchiveCompress), closes the audit record and notifies Slack.

    • ERASURE with dry_run: true (the default): posts the exact plan to Slack (consents, orders, ticket IDs and file keys) and changes nothing.

    • ERASURE with dry_run: false:

      1. Asks for approval in Slack. approval (io.kestra.plugin.core.flow.Pause) shows a form with approved, approver and notes, and cancels after 72 hours.
      2. If approved, erases in parallel:
        • a single Postgres transaction deletes the consents, clears shipping addresses on orders while keeping the order rows for financial records, and anonymizes the customer;
        • OpenSearch _delete_by_query removes the tickets;
        • DeleteList removes the files.
      3. Searches every system again. fail_on_residual fails the run if anything is left.
      4. Writes a proof-of-erasure JSON and closes the audit record.

      If rejected, the audit record is marked REJECTED with the reason, and nothing is erased.

  6. If any step fails, the errors branch marks the audit record FAILED and alerts Slack with the failed task, the error and a link to the execution.

Prerequisites

  • PostgreSQL with customers (id, email, full_name, phone, address, erased_at), orders (customer_id, shipping_address) and marketing_consents (customer_id). Adapt the SQL to your schema.
  • S3-compatible object storage with user files under customers/<customer id>/: AWS S3, or a self-hosted store such as SeaweedFS.
  • OpenSearch with a support-tickets index whose customer_email field is a keyword holding lower-case emails.
  • A Slack incoming webhook, and a Kestra worker that can reach all three systems.

Secrets

  • PG_URL: JDBC URL of the application database, for example jdbc:postgresql://db:5432/app.
  • PG_USERNAME, PG_PASSWORD: a database user allowed to read, update and delete in the three tables.
  • OPENSEARCH_URL: OpenSearch endpoint, for example http://opensearch:9200.
  • S3_ACCESS_KEY_ID, S3_SECRET_KEY_ID: object storage credentials allowed to list, read and delete in the bucket.
  • SLACK_WEBHOOK_URL: Slack incoming webhook for plans, approvals, confirmations and failure alerts.
  • DSAR_WEBHOOK_KEY: secret key in the intake webhook URL.

Inputs

  • request_id (STRING, required): the request's ticket or reference number. It is used for labels, the audit record and duplicate detection.
  • subject_email (EMAIL, required): the data subject's email, matched case-insensitively.
  • request_type (SELECT ACCESS or ERASURE, default ACCESS).
  • dry_run (BOOL, default true): for erasure, only report the plan.
  • bucket (STRING, default user-uploads).
  • s3_endpoint (STRING, default https://s3.us-east-1.amazonaws.com): the S3 API endpoint. Set it to your region's endpoint, or to an S3-compatible store such as http://seaweedfs:8333.
  • tickets_index (STRING, default support-tickets).

The webhook body maps request_id, email, type and the optional dry_run. Webhook runs use the defaults for everything else.

Quick start

  1. Create the secrets, save the flow, and set the s3_endpoint, bucket and tickets_index defaults for your environment.
  2. Run an ACCESS request for a test account and download the ZIP from the export_bundle outputs.
  3. Run an ERASURE request with dry_run: true and review the plan in Slack. Nothing changes.
  4. Run it again with dry_run: false. When Slack asks, resume the execution, tick approved, and enter your name and the identity check you did.
  5. Point your privacy form or helpdesk at /api/v1/main/executions/webhook/company.team/gdpr-dsar-orchestrator/<DSAR_WEBHOOK_KEY> with a body such as {"request_id": "REQ-1001", "email": "jane@example.com", "type": "ERASURE"}.

Expected outputs

  • {{ outputs.findings }} (flow output) / {{ outputs.findings.values }}: database_rows, tickets, ticket_ids, files and file_keys found for the subject.
  • {{ outputs.result_file }} (flow output): the internal storage URI of the export ZIP for access requests, or of the proof of erasure for completed erasures.
  • {{ outputs.export_bundle.uri }}: the ZIP with personal-data.json, support-tickets.json and files/.
  • {{ outputs.proof.uri }}: proof-of-erasure JSON with timestamps, approver, notes, counts erased per system, and residual counts (all zero).
  • KV key dsar-<request_id>: the audit record, with status IN_PROGRESS, COMPLETED, REJECTED or FAILED.

Common pitfalls and how to adapt it

  • Retention rules. Orders are kept and only their address is cleared, because tax law usually requires keeping invoices. Change the statements in erase_db to match your own legal basis for retention.
  • Postgres casts. Use CAST(x AS type) rather than x::type in the SQL, because :name is the parameter placeholder.
  • OpenSearch queries use the Request task because its body supports expressions. Keep the email field a lower-case keyword so the term query matches exactly.
  • S3-compatible stores need forcePathStyle: true. Downloads also needs compatibilityMode: true, which this flow sets.
  • The export ZIP is stored with a .tmp extension in internal storage. Rename it to .zip before sending it.
  • Partial failures. If an erasure fails partway, for example while object storage is down, the audit record says FAILED and Slack is alerted. Fix the cause and run the same request again: the database steps are skipped because they're already done, the remaining systems are erased using the stored customer ID, and verification proves nothing is left.
  • A dry run leaves the audit record IN_PROGRESS, since the request is still open. Only a completed run closes it.
  • More systems: add a branch to discover, erase and verify for each one, such as a CRM, a data warehouse or a vector store.
  • Kestra Enterprise users can put the intake form in a Kestra App instead of a webhook.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.