Give a signed-in user the ability to (a) permanently delete their own account and all associated data, and (b) export all of their data on request — the way Facebook/Instagram offer "Download your information" and "Delete account."
Primary near-term driver: I need to delete and re-register test users repeatedly to fully exercise the registration flow (registration-stepper) until I'm confident in it. Today there is no adult self-service delete, so a deleted/abandoned test identity leaves residue that blocks re-registering with the same email/username/phone. (The only existing Keycloak delete is the parent→child unlink in UnlinkChildAccountCommandHandler.)
Critical requirement: deletion must fully unblock re-registration
After a successful self-delete, re-registering with the same email, username, and phone must work end-to-end. That means clearing every re-registration blocker:
Keycloak user (email/username conflicts → 409 in KeycloakAdminService).
user-profiles (unique index on KeycloakUserId).
registrations (pending pre-registration blocks matching email/username/phone until expiry).
Scoped to the authenticated Keycloak user (sub / ClaimTypes.NameIdentifier). Remove the Keycloak user via IKeycloakAdminService.DeleteUserAsync and purge all user-owned data. Known user-linked stores (from SpikerDbContext) to cover:
Non-Mongo stores: GridFS blobs (profile images, avatars, vault files, travel docs), Redis-backed chat/message state (ChatHub), and the provisioned Gitea student git repository (GiteaStudentGitRepositoryProvisioner).
Let's centralize the collection/store list in one place so it can't silently drift as new user-scoped collections are added.
2. Export my data (on request)
GDPR data-portability: aggregate everything above into a single downloadable archive — a ZIP of JSON documents per collection plus the binary assets (images/vault/travel docs). FB/IG do this asynchronously and notify when ready; a synchronous endpoint is fine for v1 given our data volumes, but note the async option if archives get large.
Suggested implementation
Backend: new endpoints on ProfileController (or a dedicated AccountController) in SpikerSoft.Api:
GET /api/account/export → streams the ZIP archive.
DELETE /api/account → hard-deletes the authenticated user (export-then-delete optional).
MediatR commands/queries under SpikerSoft.Business/Domain/Account/... (ExportAccountDataQueryHandler, DeleteAccountCommandHandler) that fan out across the stores + Keycloak + GridFS + Redis + Gitea.
Frontend: a "Danger zone" / "Your data" section in the existing profile page (profile.component.ts, route /profile) with Download my data and Delete account actions.
For the test-user workflow we want immediate hard delete (no soft-delete grace period that would impede rapid re-registration). FB/IG-style grace period is explicitly not wanted here, but note it as a future option if real users get this.
Audit the deletion (without retaining PII) so we have a record that an account was removed.
Open questions
Child accounts: if a parent self-deletes, do we cascade-delete their managed child accounts, block deletion until children are unlinked, or reassign? Needs a decision before implementing.
Content others depend on: donations/fundraisers/contact-messages and chess games reference other users — hard-delete vs anonymize the deleted user's references?
Scope guard: should self-delete be gated behind a feature flag / restricted to non-prod initially, given the near-term goal is test-user churn?
Acceptance criteria
A signed-in user can delete their own account; afterward they can immediately re-register with the same email/username/phone with no leftover conflicts.
A signed-in user can download a complete archive of their data (JSON + binaries).
Deletion removes the Keycloak user and all user-scoped Mongo collections, GridFS assets, Redis chat state, and the Gitea student repo.
Destructive delete is confirmation-gated and audited.
Child-account behavior (open question #1) is resolved and covered by tests.
## Motivation
Give a signed-in user the ability to (a) **permanently delete their own account and all associated data**, and (b) **export all of their data** on request — the way Facebook/Instagram offer "Download your information" and "Delete account."
**Primary near-term driver:** I need to delete and re-register test users repeatedly to fully exercise the registration flow (`registration-stepper`) until I'm confident in it. Today there is **no adult self-service delete**, so a deleted/abandoned test identity leaves residue that blocks re-registering with the same email/username/phone. (The only existing Keycloak delete is the parent→child unlink in `UnlinkChildAccountCommandHandler`.)
## Critical requirement: deletion must fully unblock re-registration
After a successful self-delete, re-registering with the **same email, username, and phone** must work end-to-end. That means clearing every re-registration blocker:
- **Keycloak** user (email/username conflicts → 409 in `KeycloakAdminService`).
- `user-profiles` (unique index on `KeycloakUserId`).
- `registrations` (pending pre-registration blocks matching email/username/phone until expiry).
- `twillio-sms-opt-ins` (SMS consent keyed by `UserId` / email / username / phone / `PreRegistrationId`).
- `child-account-requests` + `parental-approval-tokens` (block reused child usernames / pending workflows).
## Scope
### 1. Delete my account (hard delete)
Scoped to the authenticated Keycloak user (`sub` / `ClaimTypes.NameIdentifier`). Remove the Keycloak user via `IKeycloakAdminService.DeleteUserAsync` and purge all user-owned data. Known user-linked stores (from `SpikerDbContext`) to cover:
- `user-profiles`, `registrations`, `twillio-sms-opt-ins`
- Learning: `user-submitted-code`, `userLessonProgress`, `offlineLessonAttempts`, `user-knowledge-progress`, `geography-knowledge-progress`, `earned-skills`
- Reading/books: `reading-progress`, `reading-preferences`, `reading-profiles`, `user-bookmarks`, `user-notes`, `user-comments`, `book-generated-quizzes`, `book-generated-quiz-attempts`
- Activity/notifications: `user-activities`, `keycloak-events`, `file-upload-scans`, `sensitive-data-audit`
- Social/sponsorship: `donations`, `fundraisers`, `vault-items`, `calendar-events`, `contact-messages`
- Games: `game-keybindings`, `hex-tower-defence-matches`, `chess-games`
- Parent/child: `child-account-requests`, `parental-approval-tokens`
- **Non-Mongo stores:** GridFS blobs (profile images, avatars, vault files, travel docs), Redis-backed chat/message state (`ChatHub`), and the provisioned **Gitea student git repository** (`GiteaStudentGitRepositoryProvisioner`).
Let's centralize the collection/store list in one place so it can't silently drift as new user-scoped collections are added.
### 2. Export my data (on request)
GDPR data-portability: aggregate everything above into a single downloadable archive — a ZIP of JSON documents per collection plus the binary assets (images/vault/travel docs). FB/IG do this asynchronously and notify when ready; a synchronous endpoint is fine for v1 given our data volumes, but note the async option if archives get large.
## Suggested implementation
- **Backend:** new endpoints on `ProfileController` (or a dedicated `AccountController`) in `SpikerSoft.Api`:
- `GET /api/account/export` → streams the ZIP archive.
- `DELETE /api/account` → hard-deletes the authenticated user (export-then-delete optional).
- MediatR commands/queries under `SpikerSoft.Business/Domain/Account/...` (`ExportAccountDataQueryHandler`, `DeleteAccountCommandHandler`) that fan out across the stores + Keycloak + GridFS + Redis + Gitea.
- **Frontend:** a "Danger zone" / "Your data" section in the existing profile page (`profile.component.ts`, route `/profile`) with **Download my data** and **Delete account** actions.
## Safety / UX
- Destructive action → require explicit confirmation (type username to confirm and/or recent re-auth).
- For the test-user workflow we want **immediate hard delete** (no soft-delete grace period that would impede rapid re-registration). FB/IG-style grace period is explicitly *not* wanted here, but note it as a future option if real users get this.
- Audit the deletion (without retaining PII) so we have a record that an account was removed.
## Open questions
1. **Child accounts:** if a parent self-deletes, do we cascade-delete their managed child accounts, block deletion until children are unlinked, or reassign? Needs a decision before implementing.
2. **Content others depend on:** donations/fundraisers/contact-messages and chess games reference other users — hard-delete vs anonymize the deleted user's references?
3. **Scope guard:** should self-delete be gated behind a feature flag / restricted to non-prod initially, given the near-term goal is test-user churn?
## Acceptance criteria
- A signed-in user can delete their own account; afterward they can immediately re-register with the same email/username/phone with no leftover conflicts.
- A signed-in user can download a complete archive of their data (JSON + binaries).
- Deletion removes the Keycloak user and all user-scoped Mongo collections, GridFS assets, Redis chat state, and the Gitea student repo.
- Destructive delete is confirmation-gated and audited.
- Child-account behavior (open question #1) is resolved and covered by tests.
Implementation opened across two PRs (not yet merged):
Backend: spikersoft-backend PR #24 — AccountDataService (cascade child-account delete, full Mongo purge, Gitea repo + mailbox + Keycloak removal), ZIP data export, AccountController (GET /api/account/export, DELETE /api/account).
Frontend: spikersoft-angular PR #81 — Danger Zone at Profile → Site Settings (bottom): "Download my data" + "Delete account" with typed-confirmation dialog, then logout for re-registration.
Decisions applied: child accounts are cascade-deleted, available everywhere (no flag/env gating). Financial records (donations, sale-receipts) are intentionally retained. Known follow-ups: orphaned GridFS blobs and Redis chat scrollback. Will close this ticket once both PRs merge to master.
Implementation opened across two PRs (not yet merged):
- Backend: spikersoft-backend PR #24 — `AccountDataService` (cascade child-account delete, full Mongo purge, Gitea repo + mailbox + Keycloak removal), ZIP data export, `AccountController` (`GET /api/account/export`, `DELETE /api/account`).
- Frontend: spikersoft-angular PR #81 — Danger Zone at Profile → Site Settings (bottom): "Download my data" + "Delete account" with typed-confirmation dialog, then logout for re-registration.
Decisions applied: child accounts are cascade-deleted, available everywhere (no flag/env gating). Financial records (donations, sale-receipts) are intentionally retained. Known follow-ups: orphaned GridFS blobs and Redis chat scrollback. Will close this ticket once both PRs merge to `master`.
spikersoft-backend PR #24 (merged 2026-06-27) — AccountDataService (cascade child-account delete, full Mongo purge, Gitea repo + mailbox + Keycloak removal), ZIP data export, AccountController (GET /api/account/export, DELETE /api/account).
spikersoft-angular PR #81 (merged 2026-06-27) — Danger Zone at Profile → Site Settings (bottom): "Download my data" + "Delete account" (typed-confirmation dialog), then logout for re-registration.
Delivered as specified: child accounts cascade-delete, available everywhere. Financial records (donations, sale-receipts) intentionally retained. Closing. Known follow-ups remain (orphaned GridFS blobs, Redis chat scrollback) — file separately if desired.
Resolved. Both PRs merged to `master`:
- spikersoft-backend PR #24 (merged 2026-06-27) — `AccountDataService` (cascade child-account delete, full Mongo purge, Gitea repo + mailbox + Keycloak removal), ZIP data export, `AccountController` (`GET /api/account/export`, `DELETE /api/account`).
- spikersoft-angular PR #81 (merged 2026-06-27) — Danger Zone at Profile → Site Settings (bottom): "Download my data" + "Delete account" (typed-confirmation dialog), then logout for re-registration.
Delivered as specified: child accounts cascade-delete, available everywhere. Financial records (donations, sale-receipts) intentionally retained. Closing. Known follow-ups remain (orphaned GridFS blobs, Redis chat scrollback) — file separately if desired.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Motivation
Give a signed-in user the ability to (a) permanently delete their own account and all associated data, and (b) export all of their data on request — the way Facebook/Instagram offer "Download your information" and "Delete account."
Primary near-term driver: I need to delete and re-register test users repeatedly to fully exercise the registration flow (
registration-stepper) until I'm confident in it. Today there is no adult self-service delete, so a deleted/abandoned test identity leaves residue that blocks re-registering with the same email/username/phone. (The only existing Keycloak delete is the parent→child unlink inUnlinkChildAccountCommandHandler.)Critical requirement: deletion must fully unblock re-registration
After a successful self-delete, re-registering with the same email, username, and phone must work end-to-end. That means clearing every re-registration blocker:
KeycloakAdminService).user-profiles(unique index onKeycloakUserId).registrations(pending pre-registration blocks matching email/username/phone until expiry).twillio-sms-opt-ins(SMS consent keyed byUserId/ email / username / phone /PreRegistrationId).child-account-requests+parental-approval-tokens(block reused child usernames / pending workflows).Scope
1. Delete my account (hard delete)
Scoped to the authenticated Keycloak user (
sub/ClaimTypes.NameIdentifier). Remove the Keycloak user viaIKeycloakAdminService.DeleteUserAsyncand purge all user-owned data. Known user-linked stores (fromSpikerDbContext) to cover:user-profiles,registrations,twillio-sms-opt-insuser-submitted-code,userLessonProgress,offlineLessonAttempts,user-knowledge-progress,geography-knowledge-progress,earned-skillsreading-progress,reading-preferences,reading-profiles,user-bookmarks,user-notes,user-comments,book-generated-quizzes,book-generated-quiz-attemptsuser-activities,keycloak-events,file-upload-scans,sensitive-data-auditdonations,fundraisers,vault-items,calendar-events,contact-messagesgame-keybindings,hex-tower-defence-matches,chess-gameschild-account-requests,parental-approval-tokensChatHub), and the provisioned Gitea student git repository (GiteaStudentGitRepositoryProvisioner).Let's centralize the collection/store list in one place so it can't silently drift as new user-scoped collections are added.
2. Export my data (on request)
GDPR data-portability: aggregate everything above into a single downloadable archive — a ZIP of JSON documents per collection plus the binary assets (images/vault/travel docs). FB/IG do this asynchronously and notify when ready; a synchronous endpoint is fine for v1 given our data volumes, but note the async option if archives get large.
Suggested implementation
ProfileController(or a dedicatedAccountController) inSpikerSoft.Api:GET /api/account/export→ streams the ZIP archive.DELETE /api/account→ hard-deletes the authenticated user (export-then-delete optional).SpikerSoft.Business/Domain/Account/...(ExportAccountDataQueryHandler,DeleteAccountCommandHandler) that fan out across the stores + Keycloak + GridFS + Redis + Gitea.profile.component.ts, route/profile) with Download my data and Delete account actions.Safety / UX
Open questions
Acceptance criteria
Implementation opened across two PRs (not yet merged):
AccountDataService(cascade child-account delete, full Mongo purge, Gitea repo + mailbox + Keycloak removal), ZIP data export,AccountController(GET /api/account/export,DELETE /api/account).Decisions applied: child accounts are cascade-deleted, available everywhere (no flag/env gating). Financial records (donations, sale-receipts) are intentionally retained. Known follow-ups: orphaned GridFS blobs and Redis chat scrollback. Will close this ticket once both PRs merge to
master.Resolved. Both PRs merged to
master:AccountDataService(cascade child-account delete, full Mongo purge, Gitea repo + mailbox + Keycloak removal), ZIP data export,AccountController(GET /api/account/export,DELETE /api/account).Delivered as specified: child accounts cascade-delete, available everywhere. Financial records (donations, sale-receipts) intentionally retained. Closing. Known follow-ups remain (orphaned GridFS blobs, Redis chat scrollback) — file separately if desired.