# How the GDPR Storage Erasure Endpoint Handles Data Deletion in Open SEO

> Learn how the GDPR storage erasure endpoint in Open SEO deletes user data. It securely handles deletion from databases and cloud storage with HMAC verification.

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

---

**The GDPR storage erasure endpoint at `/api/internal/gdpr-erasure/storage` authenticates requests via HMAC signature verification, validates payloads against a Zod schema, then permanently deletes user data from database tables and cloud storage before returning a success confirmation.**

The **GDPR storage erasure endpoint** is a private internal API in the `every-app/open-seo` repository designed for compliance with data deletion requirements. Unlike public-facing endpoints, this handler requires cryptographic authentication and performs irreversible data removal across multiple storage layers. This article examines the complete implementation based on the source code in the main branch.

## Authentication and Request Verification

The endpoint implements a two-layer security model using timestamped HMAC signatures. Every request must prove it originated from an authorized internal service.

### Header Extraction and Signature Validation

When a `POST` request arrives at `/api/internal/gdpr-erasure/storage`, the handler at [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts) immediately extracts two required headers:

- **`x-gdpr-timestamp`** — prevents replay attacks by enforcing request freshness
- **`x-gdpr-signature`** — proves the payload was signed by a trusted party

The server invokes `signGdprErasureRequest` from [`src/shared/gdpr-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts) to recompute the expected signature from the payload, timestamp, and a configured secret key. If the supplied signature fails to match, the endpoint returns **401 Unauthorized** without processing further.

```typescript
// From src/shared/gdpr-erasure.ts
export async function signGdprErasureRequest(
  payload: GdprStorageErasurePayload,
  timestamp: string
): Promise<string> {
  const message = `${timestamp}.${JSON.stringify(payload)}`;
  return createHmac('sha256', process.env.GDPR_SECRET_KEY)
    .update(message)
    .digest('hex');
}

```

## Payload Schema Validation

Before any deletion occurs, the request body undergoes strict validation against **`gdprStorageErasurePayloadSchema`** defined in [`src/shared/gdpr-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts). This Zod schema ensures:

- `userId` is present and formatted correctly
- `startedByUserId` identifies the authorized initiator
- All required metadata fields are provided

Validation failures immediately trigger a **400 Bad Request** response, preventing malformed or incomplete erasure attempts.

```typescript
// Schema definition location: src/shared/gdpr-erasure.ts
export const gdprStorageErasurePayloadSchema = z.object({
  userId: z.string().uuid(),
  startedByUserId: z.string().uuid(),
  reason: z.enum(['user_request', 'account_deletion', 'legal_requirement']),
  requestedAt: z.string().datetime(),
});

```

## Multi-Layer Data Deletion Process

Once authenticated and validated, the handler executes a cascading deletion across all data tiers. The implementation in [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts) orchestrates this through storage-layer functions.

### Database Row Removal

The erasure first targets relational data:

- **`users`** table — primary user record
- **`projects`** — all SEO projects owned by the user
- **`keywords`** — tracked keyword entries
- **`serp_snapshots`** — historical search result data

These deletions use hard `DELETE` operations rather than soft flags, ensuring true data elimination per GDPR Article 17 requirements.

### Cloud Storage Cleanup

Beyond the database, the handler removes:

- Uploaded files and blobs from configured cloud storage (D1 or Postgres-backed object stores)
- Any derivative exports or report archives linked to the user ID

### Cache Invalidation

The implementation optionally clears cached data indexed by user ID, preventing stale data from persisting in Redis or edge caches.

## Error Handling and Observability

The endpoint wraps all deletion operations in comprehensive error handling. Any exception during the process:

1. Is logged via `console.error` with full stack traces
2. Is reported through `captureServerError` with the tag `source: "gdpr_storage_erasure"`
3. Returns a generic **500 Internal Server Error** to callers (avoiding information disclosure)

This observability ensures audit trails for compliance reviews while protecting internal implementation details.

## Response Format

Successful completion returns a minimal confirmation:

```json
{
  "ok": true
}

```

The **200 OK** response indicates all storage tiers have been processed and the user's data is permanently erased.

## Route Registration and Integration

The endpoint is wired into the main application router in **[`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts)**:

```typescript
import { handleGdprStorageErasure } from "@/server/gdpr/storage-erasure";
import { GDPR_STORAGE_ERASURE_PATH } from "@/shared/gdpr-erasure";

router.post(GDPR_STORAGE_ERASURE_PATH, handleGdprStorageErasure);

```

This centralized registration pattern keeps routing logic explicit and enables straightforward middleware application.

## Calling the Endpoint Programmatically

### From Internal Services (TypeScript)

```typescript
import { signGdprErasureRequest } from "./src/shared/gdpr-erasure";

const payload = {
  userId: "12345",
  startedByUserId: "gdpr-deleted-user",
  reason: "user_request",
  requestedAt: new Date().toISOString(),
};

const timestamp = Date.now().toString();
const signature = await signGdprErasureRequest(payload, timestamp);

const response = await fetch(
  "https://your-domain.com/api/internal/gdpr-erasure/storage",
  {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-gdpr-timestamp": timestamp,
      "x-gdpr-signature": signature,
    },
    body: JSON.stringify(payload),
  }
);

const result = await response.json();
console.log("Erasure confirmed:", result.ok);

```

### Via CLI Utility

For operational use, the repository includes a dedicated script at [`scripts/erase-user-data.ts`](https://github.com/every-app/open-seo/blob/main/scripts/erase-user-data.ts):

```bash
pnpm gdpr:erase-user --email user@example.com

```

This CLI builds the signed payload automatically using environment-configured credentials and handles the complete request lifecycle with formatted output.

## Key Implementation Files

| File | Purpose |
|------|---------|
| [`src/shared/gdpr-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts) | Zod schema definitions and HMAC signing utilities |
| [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts) | HTTP handler implementing verification and deletion logic |
| [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) | Route registration with the main application router |
| [`scripts/erase-user-data.ts`](https://github.com/every-app/open-seo/blob/main/scripts/erase-user-data.ts) | CLI tool for operational erasure requests |
| [`runbooks/gdpr-erasure.md`](https://github.com/every-app/open-seo/blob/main/runbooks/gdpr-erasure.md) | Operational documentation for manual procedures and audit requirements |

## Summary

- **Authentication**: HMAC signature verification via `signGdprErasureRequest` in [`src/shared/gdpr-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/shared/gdpr-erasure.ts) prevents unauthorized erasure attempts
- **Validation**: Zod schema enforcement rejects malformed payloads before any data modification
- **Deletion scope**: Cascading removal across database tables, cloud storage, and caches ensures complete data elimination
- **Observability**: Structured error reporting with `captureServerError` maintains compliance audit trails
- **Integration**: Route registration in [`src/server.ts`](https://github.com/every-app/open-seo/blob/main/src/server.ts) provides clean architectural separation

## Frequently Asked Questions

### What happens if the signature verification fails?

The endpoint returns **401 Unauthorized** immediately without processing the request body. This prevents timing attacks and ensures only properly signed internal requests can trigger data deletion.

### Can this endpoint be called from external systems?

No. The endpoint is designed exclusively for internal platform workflows. The HMAC signing requires access to `GDPR_SECRET_KEY`, which is not exposed to external callers. External GDPR requests must flow through the platform's public-facing request intake system.

### What database tables are affected by the erasure?

The handler removes data from the `users` table and all related entities including `projects`, `keywords`, and `serp_snapshots`. The exact table list is defined in the storage-layer functions called from [`src/server/gdpr/storage-erasure.ts`](https://github.com/every-app/open-seo/blob/main/src/server/gdpr/storage-erasure.ts).

### How can operators verify an erasure completed successfully?

Operators can check server logs for the `source: "gdpr_storage_erasure"` tag in error tracking systems. Successful requests return `{"ok": true}`. For audit purposes, the [`runbooks/gdpr-erasure.md`](https://github.com/every-app/open-seo/blob/main/runbooks/gdpr-erasure.md) document outlines expected verification steps and post-erasure confirmation procedures.