# How OpenSEO Handles GDPR Storage Erasure Requests: A Complete Technical Guide

> Learn how OpenSEO handles GDPR storage erasure requests with its technical guide. Discover its API and CLI tools for secure data deletion across all storage.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: technical-guide
- Published: 2026-09-04

---

**OpenSEO fulfills GDPR storage erasure requests through a dedicated internal API endpoint and CLI tooling that authenticate, validate, and delete every piece of personal data across workflows, Durable Objects, R2 buckets, and KV stores.**

The OpenSEO platform implements a comprehensive right-to-be-forgotten mechanism designed to satisfy GDPR Article 17 requirements. By combining cryptographic request authentication with a coordinated deletion workflow spanning multiple Cloudflare-backed storage layers, the system ensures complete data removal while preventing unauthorized erasure attempts.

## Secure Authentication for GDPR Erasure Requests

All erasure requests target the internal endpoint `/api/internal/gdpr-erasure/storage` defined in [`src/shared/gdpr-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts). The system rejects any request that lacks proper cryptographic signatures or falls outside strict temporal and size constraints.

### HMAC-SHA-256 Signature Verification

The authentication flow relies on environment-based secrets and timing-safe comparison to prevent side-channel attacks. In [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts), the `authenticateRequest` function validates incoming requests using:

1. **Header extraction** – The request must include `x-gdpr-timestamp` and `x-gdpr-signature` headers
2. **Signature generation** – The `signGdprErasureRequest` helper (located in [`src/shared/gdpr-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts) lines 42-61) constructs an HMAC-SHA-256 hash from the secret stored in `env.GDPR_ERASURE_SECRET`, the timestamp, and the raw request body
3. **Timing-safe validation** – The server compares signatures using `timingSafeEqual` (lines 27-35 of [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts)) to prevent timing-based oracle attacks

### Request Expiration and Size Limits

OpenSEO enforces strict request freshness and payload constraints to mitigate replay attacks and denial-of-service vectors:

- **Temporal validity**: Requests older than 5 minutes are automatically rejected
- **Payload size**: Bodies exceeding 5 MiB return an immediate error
- **Validation logic**: These checks execute in [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts) between lines 46-66

## Payload Validation and Schema Enforcement

Before any deletion occurs, the request body must conform to `gdprStorageErasurePayloadSchema`, a strict Zod schema defined in [`src/shared/gdpr-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts) (lines 10-30). This schema enumerates every data collection potentially containing personal information:

- User ID and email address
- Organization IDs and project IDs
- Durable Object IDs (SamChat and OnboardingChat sessions)
- Audit IDs and active workflow IDs (crawls, rank-checks)
- R2 object keys
- Google OAuth account identifiers

If the payload fails validation, the server returns a `400 Bad Request` response with the message "Invalid erasure payload".

## The Complete Data Deletion Workflow

Once authenticated and validated, the `eraseStorage` function orchestrates a coordinated cleanup across all persistence backends. This ensures no orphaned data remains in active workflows, object storage, or key-value namespaces.

### Terminating Active Workflows

To prevent in-flight jobs from writing data after the erasure begins, `terminateWorkflows` (lines 58-73 of [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts)) iterates through active crawl and rank-check workflows, invoking each workflow's `terminate()` method before data deletion proceeds.

### Revoking Google OAuth Tokens

For each Google account listed in the payload, `revokeGoogleAccount` (lines 36-70) fetches a fresh access token via the internal auth service and calls the Google revocation endpoint (`https://oauth2.googleapis.com/revoke`). Tokens that cannot be minted are marked as `token_unavailable` to ensure the audit trail reflects the attempted revocation.

### Destroying Durable Objects

The system addresses Durable Objects (DOs) through their respective namespaces:

- **SamChat sessions**: Invoked via `samChat` (lines 92-96), calling `destroyForErasure()` on each instance
- **OnboardingChat sessions**: Handled by `onboardingChat` (lines 98-104) using the same destruction pattern

### Cleaning Audit Scratchpads and KV Storage

For each audit ID in the payload:

1. `getAuditScratchpad(...).destroyForErasure()` removes the temporary database row
2. The corresponding KV entry `audit-progress:<auditId>` is deleted

This logic appears in [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts) alongside the Durable Object cleanup routines.

### Removing R2 Objects and Prompt Caches

- **Cloudflare R2**: The system batches deletions in groups of up to 1,000 keys using `env.R2.delete` (lines 14-17)
- **Prompt caches**: `deleteOrganizationPromptCaches` (lines 10-34) enumerates objects under the `AI_SEARCH_PROMPT_CACHE_NAMESPACE` prefix and deletes those matching the supplied `organizationIds`

### Deleting OAuth Grants from KV

The `deleteOauthGrants` function (lines 88-108) lists all KV keys with the prefix `grant:<userId>:`, then removes each associated token key via `deleteKvPrefix`. This ensures complete removal of OAuth grant artifacts from the edge cache.

## How to Trigger a GDPR Erasure Request

Operators typically initiate erasure through two primary interfaces: a CLI utility for manual operations or direct API integration for automated systems.

### Using the CLI Script

The [`scripts/erase-user-data.ts`](https://github.com/every-app/open-seo/blob/main/scripts/erase-user-data.ts) utility automates payload assembly and request signing:

```bash
pnpm gdpr:erase-user --email user@example.com \
    --secret $GDPR_ERASURE_SECRET

```

This script queries the database to gather related IDs (organizations, projects, audits), constructs the payload, and invokes the internal endpoint. It also respects Cloudflare's 30-day state-retention window for any remaining in-flight requests (lines 150-165).

### Sending a Signed Request Programmatically

For custom integrations, construct the HMAC signature and POST to the endpoint:

```javascript
import fs from "fs";
import crypto from "crypto";
import fetch from "node-fetch";

const secret = "YOUR_GDPR_ERASURE_SECRET";
const payload = JSON.stringify({
  userId: "12345",
  email: "user@example.com",
  organizationIds: ["org-1"],
  projectIds: ["proj-1"],
  samSessionIds: [],
  auditIds: [],
  activeAuditWorkflowIds: [],
  activeRankWorkflowIds: [],
  r2Keys: [],
  googleAccounts: [],
});

const timestamp = Date.now().toString();
const hmac = crypto.createHmac("sha256", secret)
                 .update(`${timestamp}.${payload}`)
                 .digest("hex");

await fetch("https://your-open-seo-instance.com/api/internal/gdpr-erasure/storage", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Content-Length": Buffer.byteLength(payload).toString(),
    "x-gdpr-timestamp": timestamp,
    "x-gdpr-signature": hmac,
  },
  body: payload,
});

```

## Summary

- **Authentication**: OpenSEO validates GDPR storage erasure requests using HMAC-SHA-256 signatures with timing-safe comparison and 5-minute expiration windows
- **Validation**: Strict Zod schema enforcement in [`src/shared/gdpr-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts) ensures only properly formatted payloads reach the deletion stage
- **Comprehensive deletion**: The `eraseStorage` implementation removes data from active workflows, Google OAuth tokens, Durable Objects, R2 buckets, KV stores, and prompt caches
- **Operational tools**: The [`scripts/erase-user-data.ts`](https://github.com/every-app/open-seo/blob/main/scripts/erase-user-data.ts) CLI provides a convenient interface for operators to trigger erasures while respecting Cloudflare retention policies

## Frequently Asked Questions

### How does OpenSEO authenticate GDPR storage erasure requests?

OpenSEO authenticates requests through HMAC-SHA-256 signatures generated using a secret stored in `env.GDPR_ERASURE_SECRET`. The server compares the provided `x-gdpr-signature` header against a computed hash using `timingSafeEqual` to prevent timing attacks, and rejects requests older than 5 minutes or exceeding 5 MiB in size.

### What data categories does OpenSEO delete during a GDPR erasure?

The erasure workflow removes active workflow instances, Google OAuth tokens via the Google revocation endpoint, SamChat and OnboardingChat Durable Objects, audit scratchpads and KV progress markers, R2 storage objects, organization-specific prompt caches, and OAuth grants from KV storage.

### How long do GDPR erasure requests remain valid in OpenSEO?

Requests expire after 5 minutes from the timestamp specified in the `x-gdpr-timestamp` header. Additionally, the CLI script respects a 30-day Cloudflare state-retention window for any remaining in-flight requests before considering the erasure complete.

### Can OpenSEO erase data from Durable Objects and R2 storage?

Yes. The system specifically targets Durable Objects by invoking `destroyForErasure()` on SamChat and OnboardingChat instances, and removes objects from Cloudflare R2 in batches of up to 1,000 keys using the `env.R2.delete` method.