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

> Learn how OpenSEO performs GDPR data erasure across Cloudflare and PostgreSQL. Discover the coordinated deletion process and signed API pipeline for secure data removal.

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

---

**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`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts) and [`scripts/erase-user-data.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts) at lines **214‑216**, the implementation batch-deletes objects:

```typescript
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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/storage-erasure.ts) iterate through session arrays, invoking `destroyForErasure()` on each instance:

```typescript
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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts). The following Node.js script demonstrates the complete request flow:

```typescript
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:

```typescript
// 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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/scripts/erase-user-data.ts) first identifies all user-related identifiers across PostgreSQL, then delegates deletion to the [`storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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`](https://github.com/every-app/open-seo/blob/main/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.