Skip to content

feat: GDPR-style user data export & account deletion#1015

Merged
Olowodarey merged 1 commit into
Arena1X:mainfrom
Bhenzdizma:contrrrr
Jun 23, 2026
Merged

feat: GDPR-style user data export & account deletion#1015
Olowodarey merged 1 commit into
Arena1X:mainfrom
Bhenzdizma:contrrrr

Conversation

@Bhenzdizma

Copy link
Copy Markdown
Contributor

Implements a self-contained AccountModule that gives users the ability to (1) export all of their personal data as a downloadable JSON bundle and (2) permanently delete their account, anonymizing PII while preserving on-chain data integrity.

Motivation

Users generate data across predictions, markets, competitions, notifications, leaderboard, and achievements with no way to retrieve or erase it. This is a standard legal/compliance requirement (GDPR Art. 15 & 17, CCPA) and was previously impossible.

Changes

New files

File Purpose
src/account/entities/data-export-job.entity.ts Tracks async export jobs (pending → processing → ready | failed) with file path and expiry timestamp
src/account/account.service.ts Core business logic — export queuing, async processing cron, account deletion transaction, expiry cleanup cron
src/account/account.controller.ts JWT-protected REST endpoints for export and deletion
src/account/account.module.ts NestJS module wiring
src/migrations/1776300000000-AddDataExportJobAndUserSoftDelete.ts Adds deleted_at to users, creates data_export_jobs table with indexes

Modified files

File Change
src/users/entities/user.entity.ts Added @DeleteDateColumn() deleted_at — TypeORM automatically excludes soft-deleted users from all queries, invalidating their JWT immediately
src/config/env.validation.ts Added optional EXPORT_DIR (default ./exports) and EXPORT_TTL_HOURS (default 48)
src/app.module.ts Registered AccountModule

API Endpoints

All endpoints require a valid JWT (global JwtAuthGuard applies).

POST   /account/export                  → { jobId, status: "pending" }
GET    /account/export/:jobId           → { jobId, status, expires_at }
GET    /account/export/:jobId/download  → JSON file download (StreamableFile)
DELETE /account                         → 204 No Content

How it works

Data export (async)

  1. POST /account/export creates a DataExportJob row and returns the job ID. If a pending or processing job already exists for the user it is returned instead (idempotent).
  2. A @Cron(EVERY_MINUTE) worker picks up pending jobs in batches of 5. For each job it:
    • Gathers data in parallel from predictions, markets, user_achievements, user_bookmarks, user_follows, competition_participants, leaderboard_history, and notifications
    • Writes a <jobId>.json file to EXPORT_DIR
    • Updates the job to ready with an expiry timestamp (now + EXPORT_TTL_HOURS)
  3. The client polls GET /account/export/:jobId until status === "ready", then calls the download endpoint.
  4. Ownership is enforced: job.user_id !== userId → 404. Expired files → 410 Gone.
  5. A @Cron(EVERY_DAY_AT_MIDNIGHT) cleanup job deletes expired files from disk and removes their DB rows.

Account deletion

DELETE /account runs a single database transaction that:

  1. Reads the user's stellar_address (needed for address-indexed tables).
  2. Collects export file paths for later disk cleanup.
  3. Deletes notifications rows by user_address.
  4. Deletes notification_digest_state rows by user_id.
  5. Deletes all data_export_jobs for the user.
  6. Anonymizes the user row: nulls email, username, avatar_url, ban_reason, banned_at, banned_by and sets deleted_at = NOW().

After the transaction commits, export files are removed from disk.

stellar_address is intentionally preserved to maintain on-chain data integrity (predictions, market settlements). Setting deleted_at causes TypeORM's soft-delete filter to exclude the row from all subsequent queries, so the user's JWT immediately stops working — they cannot log in again.

Environment variables

Variable Default Description
EXPORT_DIR ./exports Directory where JSON export files are written
EXPORT_TTL_HOURS 48 Hours before an export file expires and is cleaned up

Both variables are optional; the service works out of the box without them.

Acceptance criteria

  • POST /account/export returns a job ID; polling shows pending → ready
  • Export JSON bundle contains profile, predictions, markets, notifications, achievements, bookmarks, follows, competitions, leaderboard history
  • Export files expire after TTL; expired download returns 410 Gone
  • DELETE /account anonymizes user, removes PII tables, user can no longer log in
  • A user cannot download another user's export — returns 404
  • Cleanup cron deletes expired export files and DB rows daily
  • Migration adds deleted_at to users and creates data_export_jobs table

closes #1005

@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
insight-arena-4rll Ready Ready Preview, Comment Jun 23, 2026 6:00am

@Olowodarey Olowodarey merged commit 03cee91 into Arena1X:main Jun 23, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend] — GDPR-style user data export & account deletion

2 participants