How OpenSEO Handles GDPR Storage Erasure Requests

OpenSEO fulfills GDPR storage erasure requests through an authenticated internal API endpoint that verifies HMAC-SHA-256 signatures, validates every payload field with a Zod schema, and then coordinates deletion across active workflows, Google OAuth tokens, Durable Objects, KV stores, and Cloudflare R2.

OpenSEO, the open-source SEO toolkit maintained in the every-app/open-seo repository, implements a strict, code-defined pipeline for handling GDPR storage erasure requests. The process is governed by src/server/gdpr/storage-erasure.ts and src/shared/gdpr-erasure.ts, which together enforce cryptographic authentication, schema validation, and exhaustive data removal.

Secure Request Authentication

The erasure flow begins at the internal POST endpoint /api/internal/gdpr-erasure/storage, defined in src/shared/gdpr-erasure.ts. Every incoming request must carry two custom headers: x-gdpr-timestamp and x-gdpr-signature.

The authenticateRequest helper constructs an HMAC-SHA-256 signature by calling signGdprErasureRequest in src/shared/gdpr-erasure.ts (lines 42–61). It derives the signature from the secret stored in env.GDPR_ERASURE_SECRET, the timestamp header, and the raw request body. The server then compares this value against the client-supplied header using Node.js timingSafeEqual to block timing side-channel attacks (src/server/gdpr/storage-erasure.ts, lines 27–35).

OpenSEO also applies hard window and size limits. Any request older than five minutes or carrying a payload larger than 5 MiB is rejected immediately (src/server/gdpr/storage-erasure.ts, lines 46–66).

Payload Validation

After authentication, the request body is parsed against gdprStorageErasurePayloadSchema, a strict Zod schema exported from src/shared/gdpr-erasure.ts (lines 10–30). The schema enumerates every data collection that can hold personal information, including:

  • userId and email
  • organizationIds and projectIds
  • Durable Object session IDs
  • auditIds and active workflow IDs
  • r2Keys
  • Google OAuth accounts

If the payload fails validation, the endpoint returns a 400 Bad Request response with the message Invalid erasure payload. Only a fully validated payload proceeds to deletion.

Comprehensive Data Removal

Once authenticated and parsed, the eraseStorage function in src/server/gdpr/storage-erasure.ts (lines 22–41) executes a coordinated cleanup across all persistence layers. It aggregates a result object summarizing deletions per category and returns it to the caller.

Active Workflows

To prevent in-flight jobs from writing data after the wipe, terminateWorkflows iterates over every active crawl and rank-check workflow ID and invokes each workflow’s terminate() method. This step is implemented in src/server/gdpr/storage-erasure.ts (lines 58–73).

Google OAuth Tokens

For each Google account listed in the payload, revokeGoogleAccount requests a fresh access token through OpenSEO’s internal auth service and then posts it to https://oauth2.googleapis.com/revoke. If a token cannot be minted, the entry is marked as token_unavailable so operators know revocation was deferred. The logic resides in src/server/gdpr/storage-erasure.ts (lines 36–70).

Durable Objects

OpenSEO targets two Durable Object namespaces during erasure: SamChat and OnboardingChat. Each instance is addressed through its namespace binding and receives a destroyForErasure() call. You can review the namespace lookups in src/server/gdpr/storage-erasure.ts (lines 92–96 for SamChat and lines 98–104 for OnboardingChat).

Audit Scratchpads and KV Progress Markers

For every auditId in the payload, the system calls getAuditScratchpad(...).destroyForErasure() to remove the temporary database row. It also deletes the matching KV key audit-progress:<auditId> from Cloudflare KV. This cleanup is handled early in the erasure sequence (src/server/gdpr/storage-erasure.ts, lines 5–9).

R2 Objects

Personal data stored in Cloudflare R2 is deleted in batches of up to 1,000 keys per operation. The function invokes env.R2.delete with the batched key list, as shown in src/server/gdpr/storage-erasure.ts (lines 14–17).

Prompt-Cache Objects

Per-organization prompt caches are stored under the prefix defined by AI_SEARCH_PROMPT_CACHE_NAMESPACE. The deleteOrganizationPromptCaches routine enumerates every object under that prefix and deletes those whose metadata matches any of the supplied organizationIds. The implementation appears in src/server/gdpr/storage-erasure.ts (lines 10–34).

OAuth Grants and Tokens in KV

All KV keys prefixed with grant:<userId>: are listed via deleteOauthGrants, and each grant’s associated token keys are removed via deleteKvPrefix. This ensures that OAuth authorizations and refresh tokens are fully purged from the edge cache. See src/server/gdpr/storage-erasure.ts (lines 88–108).

Triggering Erasure with the CLI Script

Operators typically initiate a GDPR storage erasure request through the CLI script at scripts/erase-user-data.ts. The script queries the database for every ID associated with the target user, assembles the payload, and signs the request using signGdprErasureRequest. It then posts to the internal endpoint with the required HMAC headers. The script also respects a 30-day Cloudflare state-retention window for any remaining in-flight requests (scripts/erase-user-data.ts, lines 150–165).

You can run the script directly:

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

Or send a signed request manually from Node.js:

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

  • OpenSEO exposes a single internal endpoint, /api/internal/gdpr-erasure/storage, protected by HMAC-SHA-256 signing and timingSafeEqual verification.
  • Every payload is strictly validated against the gdprStorageErasurePayloadSchema Zod schema before any data is touched.
  • The eraseStorage routine removes personal data from seven distinct backends: active workflows, Google OAuth tokens, Durable Objects, audit scratchpads, Cloudflare KV, Cloudflare R2, and prompt caches.
  • Operators invoke the process safely through scripts/erase-user-data.ts, which handles payload assembly, signing, and the 30-day retention window.

Frequently Asked Questions

What authentication mechanism protects OpenSEO's GDPR erasure endpoint?

The endpoint requires a signed POST request carrying x-gdpr-timestamp and x-gdpr-signature headers. The server rebuilds an HMAC-SHA-256 signature in signGdprErasureRequest and compares it with timingSafeEqual to prevent timing attacks. Requests older than five minutes or exceeding 5 MiB are rejected.

Which storage backends are cleaned during a GDPR storage erasure request in OpenSEO?

A single erasure request triggers cleanup across seven storage backends. These include active workflows, Google OAuth tokens, Durable Objects, audit scratchpads and KV markers, R2 object batches, prompt caches, and OAuth grants stored in KV.

How does OpenSEO handle active workflows during a GDPR erasure?

The terminateWorkflows function in src/server/gdpr/storage-erasure.ts calls terminate() on each active crawl or rank-check workflow. This stops in-flight jobs before storage layers are wiped, preventing post-erasure writes.

Can Google OAuth tokens be revoked if they are already expired?

Yes. revokeGoogleAccount attempts to mint a fresh access token through the internal auth service before calling the Google revocation endpoint. If the token cannot be refreshed, the system marks the account as token_unavailable instead of failing silently, giving operators a clear audit trail.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →