Conversation
There was a problem hiding this comment.
Pull request overview
Adds end-user “Delete Community” and “Delete Account” flows designed to preserve the scholarly record (DOI resolution + authorship attribution) by avoiding destructive cascades and introducing archive/sentinel system entities.
Changes:
- Added API endpoints and server-side destroy logic for community deletion (archive DOI pubs; delete non-DOI content) and account deletion (preserve attributions; reassign authored content to a sentinel user).
- Added schema migration to adjust FK
onDeletebehaviors and seed system entities (deleted-user + archive community). - Added UI surfaces for deleting a community (dashboard danger zone) and deleting an account (privacy settings), plus an archive banner on archived pub pages.
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| utils/api/contracts/community.ts | Adds community deletion audit + delete routes to the API contract. |
| utils/api/contracts/account.ts | Adds account deletion audit + delete routes to the API contract. |
| types/request.ts | Extends initial community payload with isArchiveCommunity. |
| tools/migrations/2026_04_13_prepareAccountAndCommunityDeletion.js | Alters FK delete behaviors and seeds sentinel/archive entities. |
| server/utils/systemEntities.ts | Centralizes UUIDs for sentinel user and archive community. |
| server/utils/initData.ts | Sets isArchiveCommunity for client rendering logic. |
| server/user/model.ts | Updates user associations’ onDelete behavior to avoid cascades. |
| server/user/destroyUser.ts | Implements account deletion preserving attributions and reassigning authored records. |
| server/user/account.ts | Wires account deletion audit + delete handlers. |
| server/user/tests/destroyUser.test.ts | Integration coverage for account deletion semantics. |
| server/threadEvent/model.ts | Changes user FK behavior to prevent cascade deletion. |
| server/threadComment/model.ts | Changes user FK behavior to prevent cascade deletion. |
| server/reviewEvent/model.ts | Changes user FK behavior to prevent cascade deletion. |
| server/review/model.ts | Changes review author FK behavior to prevent cascade deletion. |
| server/pubAttribution/model.ts | Switches attribution user FK to SET NULL to preserve credit. |
| server/collectionAttribution/model.ts | Switches attribution user FK to SET NULL to preserve credit. |
| server/discussion/model.ts | Changes discussion author FK behavior to prevent cascade deletion. |
| server/communityBan/model.ts | Changes ban actor FK behavior to prevent cascade deletion. |
| server/doi/updateUrls.ts | Adds registrar-specific URL-only DOI update helpers. |
| server/community/destroyCommunity.ts | Implements community deletion (archive DOI pubs + best-effort DOI URL updates). |
| server/community/api.ts | Wires community deletion audit + delete handlers. |
| server/community/tests/destroyCommunity.test.ts | Integration coverage for community deletion semantics. |
| client/containers/DashboardSettings/CommunitySettings/CommunitySettings.tsx | Adds “Danger zone” tab and mounts delete UI. |
| client/containers/DashboardSettings/CommunitySettings/DeleteCommunity.tsx | Implements delete community UI (audit + typed confirmation). |
| client/containers/Legal/PrivacySettings.tsx | Replaces mailto flow with interactive delete account component. |
| client/containers/Legal/DeleteAccount.tsx | Implements delete account UI (audit + password confirmation). |
| client/containers/Pub/PubDocument/PubDocument.tsx | Renders archive notice on pub pages. |
| client/containers/Pub/PubDocument/PubArchiveNotice.tsx | Implements archive banner rendering based on isArchiveCommunity. |
| client/containers/Pub/PubDocument/pubArchiveNotice.scss | Styles for the archive banner. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
client/containers/DashboardSettings/CommunitySettings/DeleteCommunity.tsx
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
This is amazing work! Just to make sure I understand this correctly:
Essentially, when an author deletes their account, it copies any attributions they had into a shadow user on those pubs, preserving the name etc but allowing the account to be removed? If so, I think it's a very good solution, as it effectively prevents them from being able to remove their name from a record in the case of a disagreement over e.g. a correction or retraction. This is a clear exception to right to be forgotten and other privacy laws in the case of academic content. The only thing I think we should consider is whether this applies to all pubs or just ones with DOIs. |
This one's a doozy. We implement two user-facing features: Delete Community and Delete Account. Both are sensitive operations that must preserve the scholarly record - specifically, DOI resolution and author attribution on published works.
The Problem
No delete functionality existed
There were no endpoints or UI for deleting a community or a user account. Account deletion was handled manually via email to
privacy@pubpub.org.The CASCADE problem
The Sequelize models used
onDelete: 'CASCADE'extensively onuserIdforeign keys. This meant that if a User row were simply destroyed, the database would silently cascade-delete all of the following:Similarly, deleting a Community would cascade through
Puband all its children, destroying DOI'd publications whose DOIs would then resolve to dead links.Additional FK issues
Several models had no
onDeletebehavior defined at all, which would cause FK constraint violations blocking deletion:userId(NOT NULL, no FK association)userId(no onDelete)userId(no onDelete)userId(no FK association)userId(no FK association)actorId(no FK association)The DOI constraint
A DOI is a permanent identifier. Per Crossref/DataCite policy, once registered, a DOI must always resolve somewhere. The
doifield lives directly on thePubmodel as a TEXT column. Destroying the pub row means the DOI becomes a dead link in citation databases worldwide. There was no server-side logic to deactivate or re-point DOIs.The attribution constraint
PubAttributionalready had standalonename,avatar, andorcidfields (originally designed for attributing non-users). ButonDelete: 'CASCADE'onuserIdmeant these records would be destroyed rather than havinguserIdset to NULL, losing the authorship information.Solution Design
Delete Community
When a community is deleted:
Pubs WITH a DOI are moved to a global archive community at
archive.pubpub.orginstead of being destroyed. Everything travels with them:This works because all these child models reference the pub via
pubId— none have acommunityIdcolumn, so they automatically follow the pub whenPub.communityIdis updated.Pubs WITHOUT a DOI are hard-deleted via the existing
destroyPub()cascade.All other community data is deleted: Pages, Collections, Members, DepositTargets, etc.
The community row is destroyed last.
The operation requires typing the exact community title to confirm, and runs inside a database transaction (all-or-nothing).
Delete Account
When a user deletes their account:
Attributions are decoupled: the user's
fullName,avatar, andorcidare copied into the standalone fields on PubAttribution and CollectionAttribution, thenuserIdis set to NULL. This applies to all attributions regardless of DOI or release status.Everything else is reassigned to a sentinel system user (
_deleted-user, id00000000-...-000000000000). This keeps all userId columns NOT NULL and avoids any need for NULL-handling in the frontend. The sentinel'sfullNameis "Deleted User", so joins render that naturally. Affected models:User-owned data with no scholarly value is explicitly deleted: ZoteroIntegration, VisibilityUser, UserScopeVisit, UserDismissable.
CASCADE handles the rest: Member, AuthToken, EmailChangeToken, UserNotification, UserSubscription, UserNotificationPreferences, FeatureFlagUser.
ActivityItem.actorId has no FK constraint, so it's left as an orphaned UUID. The UI renders missing actor lookups as "Deleted User".
The User row is destroyed last.
The operation requires password confirmation and runs inside a transaction.
Schema Migration
FK constraint changes
Column changes
None.
CommunityBan.actorIdstays NOT NULL; it's reassigned to the sentinel user rather than nulled.Seeded entities
00000000-0000-0000-0000-00000000000000000000-0000-0000-0000-000000000001archive.pubpub.orgMigration file:
tools/migrations/2026_04_13_prepareAccountAndCommunityDeletion.jsBoth the migration AND the Sequelize model decorator files were updated to stay in sync.
Files Changed
New files
server/utils/systemEntities.tsserver/community/destroyCommunity.tsgetCommunityDeletionAudit()+destroyCommunity()server/user/destroyUser.tsgetUserDeletionAudit()+destroyUser()client/containers/DashboardSettings/CommunitySettings/DeleteCommunity.tsxclient/containers/Legal/DeleteAccount.tsxtools/migrations/2026_04_13_prepareAccountAndCommunityDeletion.jsserver/community/__tests__/destroyCommunity.test.tsserver/user/__tests__/destroyUser.test.tsserver/doi/updateUrls.tsclient/containers/Pub/PubDocument/PubArchiveNotice.tsxclient/containers/Pub/PubDocument/pubArchiveNotice.scssModified model files (onDelete changes)
server/pubAttribution/model.tsserver/collectionAttribution/model.tsserver/discussion/model.tsserver/threadComment/model.tsserver/threadEvent/model.tsserver/reviewEvent/model.tsserver/review/model.tsserver/communityBan/model.tsserver/user/model.tsModified API files
utils/api/contracts/community.ts-- addeddeletionAuditandremoveroutesutils/api/contracts/account.ts-- addeddeletionAuditanddeleteAccountroutesserver/community/api.ts-- wired deletion audit + remove handlersserver/user/account.ts-- wired deletion audit + delete account handlersModified UI files
client/containers/DashboardSettings/CommunitySettings/CommunitySettings.tsx-- added "Danger zone" tabclient/containers/Legal/PrivacySettings.tsx-- replaced mailto link with interactiveDeleteAccountcomponentclient/containers/Pub/PubDocument/PubDocument.tsx-- rendersPubArchiveNoticeabove the documenttypes/request.ts--InitialCommunityDataextended withisArchiveCommunityflagserver/utils/initData.ts-- setsisArchiveCommunitybased on community IDAPI Endpoints
Community deletion
/api/communities/:id/deletionAudit/api/communities/:id{ confirmationTitle }-- must match community titleAccount deletion
/api/account/deletionAudit/api/account{ password }-- SHA3-hashed password for confirmationUI Behavior
Delete Community (Dashboard → Settings → Danger zone)
archive.pubpub.organd non-DOI pubs will be permanently deletedpubpub.orgafter deletionDelete Account (Legal → Privacy Settings)
pubpub.orgafter deletionDOI URL Re-pointing
After community deletion, DOI resolution URLs are updated to point to
https://archive.pubpub.org/pub/<slug>. This is a URL-only update that intentionally preserves all other metadata at the registrar (title, authors, collection/issue context, references, etc.).Why URL-only?
A full redeposit would overwrite the metadata at Crossref/DataCite with whatever PubPub currently has in its database. But the pub may have been registered as part of an Issue collection, with Crossref grouping metadata linking it to other articles in that issue. A full redeposit would lose that context. The URL-only approach changes just the resolution target.
Crossref
Uses the
doDOICitUploadoperation (bulk URL update format). A tab-separated text file is POSTed to the sameDOI_SUBMISSION_URLused for deposits, but with a different operation code:DataCite
Uses
PUT /dois/{id}with only theurlattribute in the JSON:API payload. All other attributes are omitted, so DataCite leaves them unchanged.Credential timing
The community's
DepositTargetrow (which holds encrypted Crossref/DataCite credentials) will be cascade-deleted when the community is destroyed. SodestroyCommunityreads the credentials before the transaction, collects the list of DOI URL updates inside the transaction, and fires the external API calls after the commit.Error handling
DOI URL updates are best-effort. They run sequentially (one DOI at a time) to avoid overwhelming Crossref's submission queue. Failures are logged with a warning listing the affected DOIs, but they don't roll back the community deletion since the transaction has already committed.
Archive Pub Banner
Pubs at
archive.pubpub.orgdisplay a Blueprint<Callout>banner above the document:The
PubArchiveNoticecomponent checkscommunityData.isArchiveCommunity(a boolean flag set by the server ingetInitialData()) and renders nothing for pubs in normal communities.The flag is set server-side in
server/utils/initData.tsby comparing the community ID againstARCHIVE_COMMUNITY_ID. This keeps the sentinel UUID on the server and avoids leaking system entity constants to client bundles.