How OpenSEO Handles GDPR Storage Erasure Requests: A Complete Technical Guide
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. 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, the authenticateRequest function validates incoming requests using:
- Header extraction – The request must include
x-gdpr-timestampandx-gdpr-signatureheaders - Signature generation – The
signGdprErasureRequesthelper (located insrc/shared/gdpr-erasure.tslines 42-61) constructs an HMAC-SHA-256 hash from the secret stored inenv.GDPR_ERASURE_SECRET, the timestamp, and the raw request body - Timing-safe validation – The server compares signatures using
timingSafeEqual(lines 27-35 ofsrc/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.tsbetween 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 (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) 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), callingdestroyForErasure()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:
getAuditScratchpad(...).destroyForErasure()removes the temporary database row- The corresponding KV entry
audit-progress:<auditId>is deleted
This logic appears in 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 theAI_SEARCH_PROMPT_CACHE_NAMESPACEprefix and deletes those matching the suppliedorganizationIds
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 utility automates payload assembly and request signing:
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:
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.tsensures only properly formatted payloads reach the deletion stage - Comprehensive deletion: The
eraseStorageimplementation removes data from active workflows, Google OAuth tokens, Durable Objects, R2 buckets, KV stores, and prompt caches - Operational tools: The
scripts/erase-user-data.tsCLI 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.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →