How the GDPR Storage Erasure Endpoint Handles Data Deletion in Open SEO
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 immediately extracts two required headers:
x-gdpr-timestamp— prevents replay attacks by enforcing request freshnessx-gdpr-signature— proves the payload was signed by a trusted party
The server invokes signGdprErasureRequest from 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.
// 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. This Zod schema ensures:
userIdis present and formatted correctlystartedByUserIdidentifies the authorized initiator- All required metadata fields are provided
Validation failures immediately trigger a 400 Bad Request response, preventing malformed or incomplete erasure attempts.
// 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 orchestrates this through storage-layer functions.
Database Row Removal
The erasure first targets relational data:
userstable — primary user recordprojects— all SEO projects owned by the userkeywords— tracked keyword entriesserp_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:
- Is logged via
console.errorwith full stack traces - Is reported through
captureServerErrorwith the tagsource: "gdpr_storage_erasure" - 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:
{
"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:
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)
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:
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 |
Zod schema definitions and HMAC signing utilities |
src/server/gdpr/storage-erasure.ts |
HTTP handler implementing verification and deletion logic |
src/server.ts |
Route registration with the main application router |
scripts/erase-user-data.ts |
CLI tool for operational erasure requests |
runbooks/gdpr-erasure.md |
Operational documentation for manual procedures and audit requirements |
Summary
- Authentication: HMAC signature verification via
signGdprErasureRequestinsrc/shared/gdpr-erasure.tsprevents 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
captureServerErrormaintains compliance audit trails - Integration: Route registration in
src/server.tsprovides 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.
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 document outlines expected verification steps and post-erasure confirmation procedures.
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 →