Account Management — Technical Reference
Where this lives
Backend
apps/backend/graphql/resolvers/user.resolver.jsno longer exists — the monolithic resolver was split into manyuser-*.resolver.jsfiles.accountStatus,breakStatus,getAccountAnalytics,deleteAccount/deleteAccountImmediately/requestAccountDeletion,deactivateAccount/reactivateAccount, andtakeBreak/endBreakare all wired inapps/backend/graphql/resolvers/user-account-management.resolver.js.apps/backend/graphql/types/account-management.type.js— only declaresAccountDeletionRequest/EmailVerificationStatus; the actual queries/mutations (deactivateAccount,reactivateAccount,deleteAccount,deleteAccountImmediately,requestAccountDeletion,cancelAccountDeletion,sendEmailVerification,resendEmailVerification,verifyEmail) live ingraphql/types/user.type.jsapps/backend/managers/user-managers/account-management.manager.js— business logic for deactivation, reactivation, scheduled/immediate deletion, and break modeapps/backend/managers/user-managers/email-verification.manager.js— email verification token generation and validation flowapps/backend/managers/user-managers/activity-tracking.manager.js— activity stats, activity heatmap, account analytics, device statsapps/backend/managers/user-managers/privacy-settings.manager.js— reset privacy/notification settings and privacy recommendationsapps/backend/managers/user-managers/memorialized-accounts.manager.js— memorialization requests, admin approve/reject, legacy-contact assignment, proof-of-death handling; persists directly onto the deceased user's row (no separate request table)apps/backend/graphql/types/memorialization.type.js+apps/backend/graphql/resolvers/memorialization.resolver.js— user-facingrequestMemorializationmutation andmemorializationRequestStatusqueryapps/backend/graphql/types/admin/memorialization-admin.type.js+apps/backend/graphql/resolvers/admin/memorialization-admin.resolver.js— admin review queue (adminGetMemorializationRequests,adminReviewMemorialization), gated onMODERATE_CONTENTapps/backend/services/email.service.js— thin backward-compatible re-export; the real implementation now lives inapps/backend/services/email/(a provider facade, default provider AWS SES, selectable viaEMAIL_PROVIDER)
Frontend
apps/frontend-nextjs/src/page-components/settings/AccountManagementPage.tsx(routed at/settings/account) —accountStatus,deactivateAccount,reactivateAccount,requestAccountDeletion/cancelAccountDeletion,deleteAccountImmediately, and break mode'stakeBreak(with an optionalautoReplyMessage)/endBreakapps/frontend-nextjs/src/page-components/settings/EmailVerificationPage.tsx(routed at/settings/email-verification) — showsisEmailVerified, callsresendEmailVerification, and consumes an emailed?token=link on mount viaverifyEmailapps/frontend-nextjs/src/page-components/settings/ActivityInsightsPage.tsx(routed at/settings/activity-insights) —getUserActivityStatsstat cards plus agetActivityHeatmapcontribution heatmapapps/frontend-nextjs/src/page-components/settings/PrivacyCheckupPage.tsx(routed at/settings/privacy-checkup) —getPrivacyRecommendationslist with arefreshRecommendationsbuttonapps/frontend-nextjs/src/page-components/settings/MemorializationPage.tsx(routed at/settings/memorialization) — submitsrequestMemorializationapps/frontend-admin/src/app/moderation/memorialization/page.tsx(routed at/moderation/memorialization) — admin review queue, callsadminGetMemorializationRequestsandadminReviewMemorialization
Technical implementation checklist
-
accountStatus— resolver wired inuser-account-management.resolver.js; queried byAccountManagementPage.tsx -
resendEmailVerification/verifyEmail— resolvers wired inuser-profile.resolver.js;EmailVerificationPage.tsxcallsresendEmailVerificationand consumes an emailed?token=link viaverifyEmail.sendEmailVerificationitself still has no frontend caller. -
deactivateAccount— resolver wired;AccountManagementPage.tsx'shandleDeactivatecalls it -
reactivateAccount— resolver wired inuser-account-management.resolver.js;AccountManagementPage.tsx's "Reactivate account" flow calls it (password-confirmed) -
requestAccountDeletion/cancelAccountDeletion— resolvers wired; both called fromAccountManagementPage.tsx -
deleteAccount— resolver wired inuser-account-management.resolver.js; despite the name this only finalizes a deletion already scheduled byrequestAccountDeletion— it throws unless the account ispending_deletionanddeletionScheduledAthas already passed. No frontend caller anywhere in the app. -
deleteAccountImmediately— resolver wired;AccountManagementPage.tsx's "Delete permanently now" button calls it directly, bypassing the grace period and thepending_deletionrequirement entirely -
takeBreak/endBreak(break mode) — resolvers wired;AccountManagementPage.tsxcallstakeBreakwithdurationDays -
autoReplyMessageon break mode —AccountManagementPage.tsx's "Take a break" flow has an optional auto-reply textarea, sent asautoReplyMessageintakeBreak'sinput -
resetPrivacySettings(PrivacySettings!, no args) — re-exposed in the schema. It used to be removed entirely (the old declaration lived ingraphql/types/privacy-settings.type.js, duplicated a same-namedPrivacySettingsInputfromuser.type.js, and corrupted the realupdatePrivacySettingsinput in the process — see the comment still in that file). It's now declared once, correctly, ingraphql/types/user.type.jsand wired inuser-privacy.resolver.js, which calls the manager method that always existed (userManager.resetPrivacySettings→privacySettingsManager.resetPrivacySettings) and remaps its snake_case result the same wayprivacySettings/updatePrivacySettingsdo.AccountPrivacyPage.tsxnow has a confirm-then-reset "Reset to defaults" button wired to it. -
resetNotificationSettings— resolver wired inuser-notifications.resolver.js; the Notifications page's "Reset notification settings to defaults" button (NotificationsSettingsPage.tsx) calls it -
getUserActivityStats/getActivityHeatmap— resolvers wired inuser-activity-tracking.resolver.js;ActivityInsightsPage.tsxcalls both (stat cards plus a GitHub-style contribution heatmap of posting activity). This is a distinct dataset from Settings → Time Management'susageStats(TimeManagementPage.tsx), which is implemented separately viatime-management.resolver.js -
getAccountAnalytics/getDeviceStats— resolvers wired (user-account-management.resolver.jsanduser-sessions.resolver.jsrespectively); no frontend caller -
getPrivacyRecommendations/refreshRecommendations— resolvers wired inuser-privacy.resolver.jsanduser-search-discovery.resolver.jsrespectively;PrivacyCheckupPage.tsxcalls both - Memorialized accounts — full GraphQL surface now exists:
requestMemorialization+memorializationRequestStatus(graphql/resolvers/memorialization.resolver.js), consumed byMemorializationPage.tsx;adminGetMemorializationRequests+adminReviewMemorialization(graphql/resolvers/admin/memorialization-admin.resolver.js, gated onMODERATE_CONTENT), consumed by the admin panel's/moderation/memorializationpage. Both delegate tomemorialized-accounts.manager.js, which persists the request directly onto the deceased user's row (memorializationStatus/memorializationRequestedBy/memorializationProof) rather than a separate table. Read-only field resolvers onUser(memorializedBy,memorializationRequestedAt,memorializationProof,legacyContactId,legacyContactAddedAtinuser-fields.resolver.js, allselfOnly()) remain in addition to the mutations.
Account status
accountStatus returns the current state of the authenticated user's account at a glance. Use this on the settings home screen to warn the user if their account is suspended (isSuspended: true) and display the reason and expiry date.
query AccountStatus {
accountStatus {
isActive isDeactivated isSuspended isDeleted
suspensionReason suspensionExpires
lastActivity accountType
}
}
accountType values: personal, creator, business.
Email verification
sendEmailVerification dispatches a verification email via AWS SES to the user's registered address. resendEmailVerification does the same but enforces a cooldown to prevent email spam. verifyEmail validates the one-time token in the email link and marks the account as verified.
mutation SendEmailVerification { sendEmailVerification { success message } }
mutation ResendEmailVerification { resendEmailVerification { success message } }
mutation VerifyEmail($token: String!) { verifyEmail(token: $token) { success message } }
Deactivate & reactivate
Deactivation hides the account from other users and removes it from search results without deleting any data. The user's content is preserved. password is required as a confirmation safeguard.
reactivateAccount restores visibility immediately — no admin approval needed.
# Hide account without deleting data (reversible)
mutation Deactivate($password: String!) {
deactivateAccount(password: $password) { success message }
}
# Restore a deactivated account
mutation Reactivate($password: String!) {
reactivateAccount(password: $password) { success message }
}
Account deletion
Deletion is a two-step process to give users a chance to reconsider. requestAccountDeletion schedules the deletion for a future date (typically 30 days out) and returns a scheduledDeletion timestamp. During this window, cancelAccountDeletion can abort the process. Once the grace period has elapsed, deleteAccount finalizes it — the resolver rejects the call unless the account is already pending_deletion and its deletionScheduledAt is in the past, so despite the name it is not a manual bypass. deleteAccountImmediately is the actual bypass: it skips the grace period and the pending_deletion requirement entirely, verifying only the password before anonymizing the account right away — this is what powers Settings → Account management's "Delete permanently now" option.
# Step 1 — schedule deletion (grace period allows cancellation)
mutation RequestDeletion($password: String!) {
requestAccountDeletion(password: $password) { success message }
}
# Cancel before the scheduled deletion date
mutation CancelDeletion {
cancelAccountDeletion { success message }
}
# Finalize a deletion after the grace period has elapsed (fails otherwise)
mutation DeleteAccount($password: String!) {
deleteAccount(password: $password) { success message }
}
# Bypass the grace period entirely — immediate, permanent deletion
mutation DeleteAccountImmediately($password: String!) {
deleteAccountImmediately(password: $password) { success message }
}
AccountDeletionRequest (returned by status queries) includes scheduledDeletion, reason, and canCancel.
Break mode
Users can put their account on a temporary break without deactivating it. See User Profile → Break mode for the full API. breakStatus returns whether a break is active, its start/end window, and the autoReplyMessage sent to anyone who messages the user during the break.
query BreakStatus {
breakStatus {
isOnBreak breakStart breakEnd breakType autoReplyMessage
}
}
Privacy settings
resetNotificationSettings reverts all notification preferences to platform defaults in one call — it backs the Notifications page's "Reset notification settings to defaults" button. resetPrivacySettings (PrivacySettings!, no args) does the equivalent for privacy settings; it's re-exposed in the schema (user.type.js) and wired in user-privacy.resolver.js, calling the privacySettingsManager.resetPrivacySettings logic that had existed unreachable since the mutation was removed — see the checklist above. AccountPrivacyPage.tsx's "Reset to defaults" button now calls it. setAllNotifications is still removed from the schema/resolvers (only an orphaned manager method remains, unreachable via GraphQL); markAllNotificationsAsRead and deleteAllNotifications (graphql/types/notification.type.js) are the closest surviving bulk notification operations, but they act on existing notifications rather than toggling settings.
# Reset all notification preferences to defaults
mutation ResetNotifications { resetNotificationSettings }
# Reset all privacy settings to defaults
mutation ResetPrivacy { resetPrivacySettings { isPrivate showActivityStatus allowMessagesFrom } }
Activity & presence
getUserActivityStats returns the user's platform usage summary for a given timeframe (day, week, month). totalTime is in seconds; mostActiveHour is 0–23. activityStreak is the number of consecutive days with activity. userId is required and must be the caller's own id unless the caller is an admin.
getActivityHeatmap returns a time-series grid ({ timeframe, days, total, maxCount, cells: [{date, count}] }, computed from the user's posts per day) suitable for rendering a GitHub-style contribution heatmap — ActivityInsightsPage.tsx renders it exactly this way. getAccountAnalytics provides a broader engagement overview and getDeviceStats breaks down usage by device type; neither has a frontend caller yet.
query ActivityStats($userId: ID!, $timeframe: String) {
getUserActivityStats(userId: $userId, timeframe: $timeframe) {
totalTime # seconds
activeDays
mostActiveHour # 0-23
activityStreak # consecutive days
lastActivity
}
}
query ActivityHeatmap($timeframe: String) { getActivityHeatmap(timeframe: $timeframe) }
query AccountAnalytics($timeframe: String) { getAccountAnalytics(timeframe: $timeframe) }
query DeviceStats($timeframe: String) { getDeviceStats(timeframe: $timeframe) }
Privacy recommendations
getPrivacyRecommendations returns a ranked list of suggested privacy improvements based on the user's current settings. Each recommendation has a priority (high, medium, low) and an isApplied flag showing whether the user has already adopted it. refreshRecommendations (resolver in user-search-discovery.resolver.js, returns Boolean!) recomputes them. PrivacyCheckupPage.tsx calls both.
query PrivacyRecommendations {
getPrivacyRecommendations {
id recommendationType title description priority isApplied
}
}
mutation RefreshRecommendations { refreshRecommendations }
Memorialized accounts
Any authenticated user can submit a request to memorialize a deceased user's account; approval is an admin-only action on a separate admin schema. The request is stored directly on the deceased user's row rather than in its own table — memorializationStatus (none/pending/approved/rejected), memorializationRequestedBy, memorializationRequestedAt, and a memorializationProof JSON blob (proof of death, requester identification, relationship, etc.). Approved accounts get isMemorialized: true and, unless the admin turns it off, showMemorialBanner: true, which PublicProfilePage.tsx uses to render a "Remembering" banner.
# User-facing: submit a request
mutation RequestMemorialization($input: MemorializationRequestInput!) {
requestMemorialization(input: $input) {
success message requestId estimatedReviewTime submittedAt
}
}
query MemorializationRequestStatus($deceasedUserId: ID!) {
memorializationRequestStatus(deceasedUserId: $deceasedUserId) {
hasRequest canSubmitNew
request { status submittedAt reviewedAt rejectionReason }
}
}
# Admin-only: review queue (requires the MODERATE_CONTENT permission)
query AdminMemorializationQueue($limit: Int, $offset: Int) {
adminGetMemorializationRequests(limit: $limit, offset: $offset) {
totalCount
requests { deceasedUserId deceasedUser { username } status requestedBy requestedAt relationship proof }
}
}
mutation AdminReviewMemorialization($deceasedUserId: ID!, $decision: String!) {
adminReviewMemorialization(deceasedUserId: $deceasedUserId, decision: $decision) {
success message status reviewedAt
}
}