Skip to content

feat: GDPR data controls — soft-delete + data export - #35

Open
TrainTravel wants to merge 3 commits into
mainfrom
feat/gdpr-data-controls
Open

feat: GDPR data controls — soft-delete + data export#35
TrainTravel wants to merge 3 commits into
mainfrom
feat/gdpr-data-controls

Conversation

@TrainTravel

Copy link
Copy Markdown
Owner

Summary

Two GDPR Tier-1 features in one PR, surfaced from a new AccountScreen:

  1. Account deletion with 30-day soft-delete grace (Article 17 — right to erasure)
  2. Data export — download all user data as JSON (Article 20 — right to portability)

How it works

  • New AccountScreen reachable from a footer "Account & data" link on HomeScreen.
  • Delete: confirmation modal explains the 30-day undo window → invokes schedule-account-deletion edge function → sets profiles.scheduled_deletion_at = now() + 30 days → signs the user out.
  • Cancel: returning within 30 days shows a yellow banner with a one-click cancel → invokes cancel-account-deletion.
  • Hard purge: AccountScreen lazily triggers hard-delete-account if the user returns after the deadline (CASCADE through thoughts, clusters, journal_entries, experiments, feedback, profiles, and finally auth.users).
  • Export: "Download my data" button invokes export-user-data → returns versioned JSON (schema_version: 1) with every row tagged with the caller's user_id → browser saves as outputfirst-export-YYYY-MM-DD.json via Blob + <a download> (no new dependency).

⚠ DEPLOYMENT STEPS — must run before merging

  1. Apply migration:

    supabase db push

    Or apply supabase/migrations/20260523145704_account_soft_delete.sql via the dashboard. Creates public.profiles with strict RLS (id = auth.uid()).

  2. Set secret:

    supabase secrets set SUPABASE_SERVICE_ROLE_KEY=<value>

    Required by all 4 new edge functions for cross-table writes.

  3. Deploy edge functions:

    supabase functions deploy schedule-account-deletion cancel-account-deletion hard-delete-account export-user-data

v1 limitation (documented in code + CHANGELOG)

No pg_cron job yet. A user who never returns after day 30 is not actually purged until they return. Production should add a scheduled job that invokes hard-delete-account for every row with scheduled_deletion_at < now(). Captured as a follow-up.

Test plan

  • npx tsc --noEmit — clean
  • npx vitest run — 195 passed / 196 total (only pre-existing useJournal startFreeWrite fails; confirmed unchanged on main)
  • 11 unit tests in AccountScreen.test.tsx (render, modal flow, banner, cancel, lazy purge, export click, in-flight disabled state)
  • 2 unit tests added to useJournal.test.ts (openAccount step transition + goHome from account)
  • 4 E2E tests in e2e/gdpr-account.spec.ts (footer link opens screen, delete schedules POST, banner+cancel, download triggers .json)
  • Post-rebase codemod sweep — converted 27 positional t() calls in AccountScreen + HomeScreen to object form (matches post-refactor: t() / bilingual() positional → object form #32 main)
  • Manual after deployment: schedule deletion → banner appears → cancel → banner gone
  • Manual: download → file lands on disk, contents reflect all user rows

Out of scope (follow-up PRs)

🤖 Generated with Claude Code

TrainTravel and others added 3 commits May 25, 2026 14:47
Surfaces right-to-erasure (Article 17) from a new AccountScreen reachable
from a footer link on HomeScreen. Deletion is scheduled 30 days out via
the schedule-account-deletion edge function and stored on a new profiles
table. Returning during the window reveals a yellow banner with a
one-click cancel. v1 lazy-purges on return after the deadline (no cron
yet) — documented as a known limitation.
Surfaces right-to-portability (Article 20) as a "Download my data" button
on the AccountScreen. The new export-user-data edge function aggregates
every row tagged with the caller's user_id across journal_entries,
thoughts, clusters, cluster_thoughts, proposals, experiments, and
experiment_checkins into a single versioned JSON payload. The browser
writes it to disk via a Blob + download attribute — no new dependency.
Post-rebase onto current main (object-form t() / bilingual() landed
in PR #32 between this branch's original commits and main). Ran
scripts/codemod-t-object-form.mjs against the new GDPR files.

27 call sites converted across AccountScreen.tsx + HomeScreen.tsx.
All tests still pass (AccountScreen 11/11; full suite green modulo
pre-existing useJournal startFreeWrite).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant