# How the GDPR Storage Erasure Endpoint Cascades Deletes Across D1, Hyperdrive, and OAuth KV

> Discover how Open SEO's GDPR storage erasure endpoint cascades deletes across D1 Hyperdrive and OAuth KV. Learn about coordinated purging of data, DO sessions, R2 objects, and OAuth grants.

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

---

**The GDPR storage erasure endpoint in Open SEO orchestrates a coordinated purge across Durable Objects, R2 storage, and OAuth KV by invoking `eraseStorage` in [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts), which terminates active workflows, destroys DO sessions, batch-deletes R2 objects, and recursively wipes OAuth grants and tokens in a single atomic request.**

When handling data privacy requests under GDPR, the Open SEO platform (available at `every-app/open-seo`) provides a comprehensive storage erasure mechanism. The `/api/internal/gdpr-erasure/storage` endpoint triggers a cascading deletion sequence that ensures user data is removed from every persistent layer—including Cloudflare Durable Objects, Hyperdrive-backed R2 storage, and OAuth KV namespaces.

## How the GDPR Storage Erasure Endpoint Works

The erasure flow begins when the `handleGdprStorageErasure` handler receives a validated POST request. The handler parses the payload using `gdprStorageErasurePayloadSchema`, then delegates to the core `eraseStorage` function. This function executes deletions in a specific order to prevent data inconsistency: first terminating active workflows, then purging storage layers, and finally revoking external OAuth grants.

The coordinated approach ensures that once the endpoint returns a success response, the user’s personal data has been purged from every persisted layer used by the application.

## Cascading Deletions Across Storage Layers

The `eraseStorage` function implements distinct deletion strategies for each storage backend, handling dependencies between systems to ensure complete data removal.

### Durable Objects (D1) Cleanup

For Durable Objects, the endpoint targets two specific session types:

- **Sam-chat sessions**: For each `samSessionId` provided in the payload, the code obtains the DO via `env.SAM_CHAT` and invokes `destroyForErasion()` (lines 191–198). This permanently destroys the session state stored in the Durable Object.
- **Audit scratchpad DOs**: For each `auditId`, the system calls `env.AUDIT_ENGINE.destroyScratchpad(auditId)` (lines 199–202), removing temporary computation state associated with audit workflows.

These operations run sequentially within the erasure flow to ensure that all session artifacts are cleared before proceeding to object storage cleanup.

### Hyperdrive (R2) Object Removal

The Hyperdrive-backed R2 cleanup handles both user-generated content and application cache entries:

- **User-generated objects**: The endpoint batch-deletes specific R2 keys provided in the `r2Keys` payload array using `env.R2.delete` (lines 218–220).
- **Prompt-cache cleanup**: For each `organizationId` in the payload, `deleteOrganizationPromptCaches` lists all objects under the `AI_SEARCH_PROMPT_CACHE_NAMESPACE` prefix, filters them by organization membership, and removes them in bulk (lines 221–224). The function returns the total count of removed cache objects via `promptCacheObjects` (lines 242–244) for audit reporting.

### OAuth KV Grant and Token Purge

The OAuth cleanup requires recursive deletion to handle the grant-token relationship:

- **Grant enumeration**: `deleteOauthGrants` iterates over all keys matching the prefix `grant:{userId}:` (lines 96–108).
- **Token cascade**: For each grant discovered, the system calls `deleteKvPrefix` to wipe associated token keys matching `token:{userId}:{grantId}:` (lines 100–103).
- **Grant removal**: After purging dependent tokens, the grant itself is deleted (line 104).

This ensures that orphaned tokens cannot persist after their parent grants are removed, maintaining referential integrity during the erasure process.

## Pre-Cleanup Workflow Termination

Before storage deletion begins, the endpoint prevents data resurrection by terminating active computations:

- **Audit workflows**: `terminateWorkflows` is invoked on `env.SITE_AUDIT_WORKFLOW` (lines 176–184).
- **Rank-check workflows**: The same termination logic applies to `env.RANK_CHECK_WORKFLOW`.

Stopping these workflows prevents them from recreating data after the storage wipe initiates.

## Additional Cascade Steps

The erasure process includes several ancillary cleanup operations:

- **Google account revocation**: Each Google account in the payload is revoked via the Google OAuth revocation endpoint through `revokeGoogleAccount` (lines 138–172).
- **Dub referral cleanup**: The system deletes Dub-referral keys for both the user and associated organizations using `env.KV.delete` (lines 213–216).

## Calling the GDPR Storage Erasure Endpoint

### cURL Request Example

Invoke the endpoint with proper HMAC signing headers:

```bash
curl -X POST https://your-open-seo-instance.com/api/internal/gdpr-erasure/storage \
  -H "Content-Type: application/json" \
  -H "x-gdpr-timestamp: $(date +%s%3N)" \
  -H "x-gdpr-signature: $(node -e \"const {signGdprErasureRequest}=require('./src/shared/gdpr-erasure'); signGdprErasureRequest(process.env.GDPR_ERASURE_SECRET, Date.now().toString(), JSON.stringify(<payload>)).then(s=>process.stdout.write(s))\")" \
  -d '{"userId":"user_123","organizationIds":["org_456"],"samSessionIds":["sam_abc"]}'

```

### Payload Structure

The payload must conform to `gdprStorageErasurePayloadSchema`:

```typescript
import { gdprStorageErasurePayloadSchema } from '@/shared/gdpr-erasure';

const payload = {
  userId: 'user_123',
  organizationIds: ['org_456'],
  activeAuditWorkflowIds: [],
  activeRankWorkflowIds: [],
  samSessionIds: ['sam_abc'],
  auditIds: ['audit_xyz'],
  r2Keys: ['uploads/user_123/file.png'],
  googleAccounts: [
    { providerId: 'google', accountId: 'google_789' }
  ]
};

gdprStorageErasurePayloadSchema.parse(payload); // validates

```

### Programmatic Invocation

For internal scripts, use the signing utilities provided in the shared module:

```typescript
import { signGdprErasureRequest, GDPR_STORAGE_ERASURE_PATH } from '@/shared/gdpr-erasure';

const response = await fetch(`${env.BASE_URL}${GDPR_STORAGE_ERASURE_PATH}`, {
  method: 'POST',
  headers: {
    'content-type': 'application/json',
    'x-gdpr-timestamp': timestamp,
    'x-gdpr-signature': await signGdprErasureRequest(secret, timestamp, JSON.stringify(payload)),
  },
  body: JSON.stringify(payload),
});

```

## Key Implementation Files

The GDPR erasure logic spans several critical files in the `every-app/open-seo` repository:

- **[`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts)**: Contains the core `eraseStorage` function, workflow termination logic, and coordinated deletion orchestration.
- **[`src/shared/gdpr-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts)**: Defines the `gdprStorageErasurePayloadSchema`, HMAC signing helper (`signGdprErasureRequest`), and the endpoint path constant (`GDPR_STORAGE_ERASURE_PATH`).
- **[`src/lib/oauth-resource.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/oauth-resource.ts)**: Provides the `env.OAUTH_KV` namespace used by the `deleteOauthGrants` function.
- **[`src/lib/r2-cache.ts`](https://github.com/every-app/open-seo/blob/main/src/lib/r2-cache.ts)**: Supplies the `AI_SEARCH_PROMPT_CACHE_NAMESPACE` constant referenced by `deleteOrganizationPromptCaches`.
- **[`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts)**: Registers the route handler (`handleGdprStorageErasure`) for the internal endpoint.
- **[`scripts/erase-user-data.ts`](https://github.com/every-app/open-seo/blob/main/scripts/erase-user-data.ts)**: Example administrative script demonstrating payload assembly and authenticated invocation.

## Summary

- The GDPR storage erasure endpoint at `/api/internal/gdpr-erasure/storage` processes deletion requests through `handleGdprStorageErasure` in [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts).
- **Durable Objects** are purged via `destroyForErasion()` for chat sessions and `destroyScratchpad()` for audit engines.
- **R2 storage** cleanup uses `env.R2.delete` for user objects and filtered prefix deletion for organization prompt caches.
- **OAuth KV** requires recursive deletion of grants and their dependent tokens using prefix scanning.
- Active workflows are terminated before storage deletion to prevent data resurrection during the erasure process.
- All operations require HMAC-signed requests using `signGdprErasureRequest` from [`src/shared/gdpr-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts).

## Frequently Asked Questions

### What storage systems does the GDPR erasure endpoint target?

The endpoint targets **Cloudflare Durable Objects** (for session and scratchpad state), **R2 object storage** (for user uploads and AI prompt caches), and **OAuth KV** (for grant and token persistence). It also clears Dub referral tracking data from general KV storage.

### How does the endpoint prevent data resurrection during erasure?

The system calls `terminateWorkflows` on both `env.SITE_AUDIT_WORKFLOW` and `env.RANK_CHECK_WORKFLOW` (lines 176–184) before executing any storage deletions. This ensures that active background jobs cannot recreate data after the purge begins, effectively freezing the data state before erasure.

### What authentication is required to call the GDPR storage erasure endpoint?

Requests must include an `x-gdpr-timestamp` header and an `x-gdpr-signature` header generated by the `signGdprErasureRequest` function. The signature is computed using a shared secret and the request timestamp to prevent replay attacks and unauthorized erasure requests.

### How are OAuth tokens and grants related to a user deleted?

The `deleteOauthGrants` function iterates over all KV keys matching the prefix `grant:{userId}:`. For each grant found, it first deletes all associated tokens matching `token:{userId}:{grantId}:` via `deleteKvPrefix`, then removes the grant record itself. This cascade ensures no orphaned authentication tokens remain after the parent grants are removed.