How OpenSEO Handles GDPR Data Erasure Across Cloudflare and PostgreSQL Storage Systems

OpenSEO orchestrates complete GDPR erasure by coordinating deletions across five distinct backends—Cloudflare R2, KV, Durable Objects, PostgreSQL, and external vendor APIs—using a signed API pipeline implemented in src/server/gdpr/storage-erasure.ts and scripts/erase-user-data.ts.

The every-app/open-seo repository provides a comprehensive data deletion architecture designed to satisfy Article 17 "Right to erasure" obligations under GDPR. When a user requests complete data removal, the platform executes a coordinated cleanup that touches every storage layer where personal identifiers may reside, from object storage to third-party SaaS replicas.

Internal Storage Backends

OpenSEO runs on Cloudflare's edge infrastructure and relies on four internal storage systems that each require specific erasure logic.

Cloudflare R2 Object Storage

Cloudflare R2 persists audit payloads and AI prompt caches containing user data. The erasure handler identifies targets through the payload.r2Keys array and deletes objects in batches to avoid rate limiting.

In src/server/gdpr/storage-erasure.ts at lines 214‑216, the implementation batch-deletes objects:

for (let i = 0; i < payload.r2Keys.length; i += 1_000) {
  await env.R2.delete(payload.r2Keys.slice(i, i + 1_000));
}

Additionally, lines 110‑115 clean organization-specific prompt-cache entries, ensuring cached AI interactions are purged from R2.

Cloudflare KV Store

The Cloudflare KV namespace holds transient OAuth grants, tokens, and audit-progress markers. The erasure logic targets two key patterns: grant:<userId>:* for OAuth credentials and audit-progress:<auditId> for active audit tracking.

Lines 88‑107 in storage-erasure.ts handle OAuth grant deletion, while lines 205‑208 remove KV entries tracking audit progress for the specified identifiers.

Durable Objects

Durable Objects maintain long-lived state for Sam chat sessions and onboarding conversations. Each instance must be explicitly destroyed to clear both memory and persistent storage.

Lines 94‑104 in storage-erasure.ts iterate through session arrays, invoking destroyForErasure() on each instance:

for (const sessionId of payload.samSessionIds) {
  const chat = env.samChat.get(env.samChat.idFromString(sessionId));
  await chat.destroyForErasure();
}

PostgreSQL Relational Database

The PostgreSQL layer contains core user records, organizations, projects, and API keys. The erasure script executes a database transaction defined in scripts/erase-user-data.ts at lines 68‑138 that performs the following operations:

  • Deletes user rows, invitations, and verification records
  • Removes organization memberships and deletes organizations left empty
  • Anonymizes audit startedByUserId fields to preserve business records without personal identifiers
  • Purges GSC/GA4 connections and API keys

External Vendor Integrations

Beyond Cloudflare infrastructure, OpenSEO invokes third-party deletion endpoints to clean replicated data from external services.

  • Loops: Deletes contact records via API using deleteLoopsContact
  • PostHog: Removes person records from analytics using deletePostHogPerson
  • Autumn/Stripe: Deletes customer records and associated subscriptions using deleteAutumnCustomer
  • Google OAuth: Revokes active token grants using revokeGoogleAccount

These operations reside in helper functions within scripts/erase-user-data.ts and execute after internal storage cleanup completes.

GDPR Erasure Execution Flow

The deletion process follows a strict orchestration pattern to ensure complete data removal across all systems.

Building and Signing the Erasure Payload

Client applications construct a GDPR payload containing all user-related identifiers including userId, email, organizationIds, projectIds, samSessionIds, auditIds, r2Keys, and googleAccounts.

Requests are authenticated using HMAC-SHA256 signatures generated by signGdprErasureRequest from src/shared/gdpr-erasure.ts. The following Node.js script demonstrates the complete request flow:

import { signGdprErasureRequest } from '@/shared/gdpr-erasure';
import fetch from 'node-fetch';

const payload = {
  userId: 'user-123',
  email: 'alice@example.com',
  organizationIds: ['org-1'],
  projectIds: ['proj-7'],
  samSessionIds: ['sam-42'],
  auditIds: ['audit-9'],
  activeAuditWorkflowIds: ['wf-audit-1'],
  activeRankWorkflowIds: ['wf-rank-2'],
  r2Keys: ['audit/lighthouse/abc123.json'],
  googleAccounts: [{ providerId: 'gsc', accountId: 'gsc-99' }],
};

const secret = process.env.GDPR_ERASURE_SECRET!;
const timestamp = String(Date.now());
const signature = await signGdprErasureRequest(secret, timestamp, JSON.stringify(payload));

await fetch('https://<better-auth-url>/api/internal/gdpr-erasure/storage', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-gdpr-timestamp': timestamp,
    'x-gdpr-signature': signature,
  },
  body: JSON.stringify(payload),
});

Handler Coordination

The handleGdprStorageErasure function receives requests at the /api/internal/gdpr-erasure/storage endpoint (defined as GDPR_STORAGE_ERASURE_PATH). After validating the signature, it delegates to eraseStorage, which executes operations in the following sequence:

  1. Terminates active workflows
  2. Revokes Google OAuth tokens
  3. Destroys Durable Objects
  4. Deletes KV entries
  5. Removes R2 objects and prompt caches
  6. Returns a summary of deletions performed

Operator-Level Execution

For administrative operations, the repository includes a command-line script that automates the entire pipeline:

// Run with: pnpm gdpr:erase-user --email alice@example.com
import { main as eraseUser } from '@/scripts/erase-user-data';

await eraseUser();

This script builds the complete inventory of user data across PostgreSQL and external systems, prints a dry-run summary, and optionally executes eraseWorkerStorage followed by the database transaction.

Summary

  • Five storage backends are purged during OpenSEO GDPR erasure: Cloudflare R2, KV, Durable Objects, PostgreSQL, and external vendor APIs (Loops, PostHog, Autumn, Google).
  • Coordinated deletion is handled by handleGdprStorageErasure in src/server/gdpr/storage-erasure.ts, which processes signed POST requests to /api/internal/gdpr-erasure/storage.
  • R2 objects are deleted in batches of 1,000 to avoid API limits, while Durable Objects are individually destroyed using destroyForErasure().
  • PostgreSQL cleanup uses a transaction at lines 68‑138 of scripts/erase-user-data.ts to ensure referential integrity while anonymizing audit history.
  • Request authentication requires HMAC-SHA256 signatures via signGdprErasureRequest to prevent unauthorized deletion operations.

Frequently Asked Questions

How does OpenSEO ensure complete GDPR erasure across distributed systems?

OpenSEO implements an inventory-based payload system where scripts/erase-user-data.ts first identifies all user-related identifiers across PostgreSQL, then delegates deletion to the storage-erasure.ts worker handler for Cloudflare-native stores. This explicit targeting ensures R2 objects, KV keys, and Durable Object instances are removed rather than relying on cascading deletes or eventual consistency.

What happens to audit history when a user requests data erasure?

According to lines 68‑138 in scripts/erase-user-data.ts, OpenSEO anonymizes audit records by nullifying the startedByUserId field rather than deleting audit rows. This preserves historical SEO audit data for business continuity while severing the link to the specific user, satisfying GDPR requirements for anonymization.

Which external services does OpenSEO contact during GDPR erasure?

The platform invokes four external APIs: Loops for email contact deletion, PostHog for analytics person removal, Autumn (Stripe) for customer and subscription deletion, and Google OAuth for token revocation. These calls occur within scripts/erase-user-data.ts through dedicated helpers like deleteLoopsContact and revokeGoogleAccount.

How are erasure requests authenticated in OpenSEO?

All requests to /api/internal/gdpr-erasure/storage must include HMAC-SHA256 signatures generated by signGdprErasureRequest from src/shared/gdpr-erasure.ts. The handler validates the x-gdpr-timestamp and x-gdpr-signature headers against a shared secret before executing any deletion operations, ensuring only authorized clients can trigger data erasure.

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 →