User Profile — Technical Reference
Where this lives
Backend
apps/backend/graphql/resolvers/user.resolver.jswas split into several per-concern files, all auto-loaded and merged bygraphql/resolvers.js(loadFilesSync+mergeResolvers), so there is no single "user resolver" file anymore:user-profile.resolver.js—me,user,userByUsername,updateProfile,isEmailVerifieduser-social.resolver.js—userStats,relationshipStatus, follow/block/mute/close-friends mutations,updateInterestsuser-search-discovery.resolver.js—searchUsers,suggestedUsers,interestSuggestions,getTrendingUsers,getRecommendedUsersuser-activity-tracking.resolver.js—getOnlineUsersand related presence trackinguser-account-management.resolver.js— deactivate/reactivate/delete account, break modeuser-privacy.resolver.js—updatePrivacySettingsuser-mentions-tags.resolver.js—tagPermissions,updateTagPermissions,removeTag,taggedInPostsuser-favorites.resolver.jsanduser-bulk-operations.resolver.js— favorites and bulk follow/block/mute operations
apps/backend/graphql/types/user.type.js—UserGraphQL schema and core profile fieldsapps/backend/graphql/types/user-features.type.js—TagPermissions,OnlineUser,TrendingUsertypes and related queries/mutationsapps/backend/graphql/types/social-actions.type.js—BulkOperationResult/MuteActionResponseand thebulkBlockUsers/bulkRemoveFollowers/bulkProcessFollowRequestsmutations (the rest of follow/block/mute/close-friends/favorites schema now lives inuser.type.jsandfavorites.type.js)apps/backend/graphql/types/appeal.type.js/appeal.resolver.js— restriction-appeal schema and resolvers, replacing the old broken stubs that used to live incontacts-validation.type.jsapps/backend/graphql/types/social-link.type.js/social-link.resolver.js— the now-liveSocialLinkGraphQL schema and resolversapps/backend/data-access-services/user/user.access-service.js—Userdatabase queriesapps/backend/data-access-services/user/social-link.access-service.js— social link persistence, used bymanagers/user-managers/social-link.manager.jsand exposed bysocial-link.resolver.jsaboveapps/backend/managers/user-managers/profile-view.manager.js— records profile visits (skipping self-views and honoring thehideProfileVisitsprivacy toggle) and lists a user's viewers, de-duplicatedapps/backend/data-access-services/user/user-profile-view.access-service.js— append-onlyuser_profile_viewlog with a 60s debounce on repeat visits from the same viewerapps/backend/graphql/types/user-profile-view.type.js/user-profile-view.resolver.js—myProfileViewers/myProfileViewerCountqueries andrecordProfileVisit/clearMyProfileViewersmutations
Frontend
apps/frontend-nextjs/src/page-components/ProfilePage.tsx— the authenticated user's own profile pageapps/frontend-nextjs/src/page-components/PublicProfilePage.tsx— viewing another user's profile (follow/message/block actions)apps/frontend-nextjs/src/page-components/settings/EditProfilePage.tsx— profile editing UI, callsupdateProfileapps/frontend-nextjs/src/app/profile/page.tsx—/profilerouteapps/frontend-nextjs/src/app/[username]/page.tsx—/[username]public profile routeapps/frontend-nextjs/src/page-components/settings/ProfileViewersPage.tsx— listsmyProfileViewers, linked from the own-profile menu (ProfilePage.tsx) at/settings/profile-viewersapps/frontend-nextjs/src/page-components/PublicProfilePage.tsx— firesrecordProfileVisiton mount when viewing someone else's profile
Technical implementation checklist
-
me— resolver wired inuser-profile.resolver.js; rendered byProfilePage.tsx -
updateProfile— mutation wired;EditProfilePage.tsx'shandleSavecalls it on the "Save changes" button (this doc previously claimed noonClickhandler existed — corrected) -
userByUsername— resolver wired; rendered byPublicProfilePage.tsx -
isVerifiedbadge — rendered next to the username wheneverisVerifiedis true, in bothProfilePage.tsxandPublicProfilePage.tsx - Social links (
addSocialLink/updateSocialLink/deleteSocialLink/mySocialLinks/userSocialLinks) — now live end to end:social-link.resolver.js+social-link.type.jsexpose the schema oversocial-link.manager.js/social-link.access-service.js, andSocialLinksPage.tsx/PublicProfilePage.tsxconsume it (this doc previously claimed no GraphQL exposure existed — corrected) -
updatePrivacySettings— mutation wired;AccountPrivacyPage.tsxcalls it viauseMutationwith real toggle handlers (this doc previously claimed static placeholders — corrected; the page was substantially built out during the Settings pass, see Settings) -
followUser/unfollowUser— mutations wired;PublicProfilePage.tsx's follow button callshandleFollowClick, which invokes them (this doc previously claimed noonClickhandler existed — corrected) - Profile visitors —
PublicProfilePage.tsxfiresrecordProfileVisiton mount;ProfileViewersPage.tsxlistsmyProfileViewers; thehideProfileVisitstoggle onAccountPrivacyPage.tsxmakesrecordVisita no-op for that user's own visits to other profiles
User model fields
Identity
| Field | Description |
|---|---|
username | Unique username |
firstName / lastName | Full name |
bio | Profile bio |
profilePicture | Profile picture URL |
coverPhoto | Cover photo URL |
dateOfBirth / gender | Demographic info |
accountType | Account type (personal, creator, etc.) |
Account status
| Field | Description |
|---|---|
accountStatus | Current account state |
isVerified / verificationStatus | Verification badge |
verificationCategory | Badge category (e.g. creator, business) |
isPrivate | Private account (requires follower approval) |
isEmailVerified / isPhoneVerified | Contact verification |
isOnboardingCompleted / onboardingStep | Onboarding progress |
Activity
| Field | Description |
|---|---|
isOnline | Currently online |
lastActiveAt | Last seen timestamp |
lastPostAt | Date of last post |
engagementScore | Calculated engagement score |
locale / timezone | Regional settings |
Security
| Field | Description |
|---|---|
twoFactorEnabled | 2FA enabled |
securityNotificationsEnabled | Security alerts active |
isTermsConditionsAccepted | T&C accepted |
Restrictions & suspension
| Field | Description |
|---|---|
suspensionCount / suspendedAt | Suspension history |
isRestricted / restrictionTypes | Active restrictions |
tagApprovalRequired | Must approve tags before they appear |
autoRemoveTags | Auto-removes tags from content |
Break mode
Allows temporarily pausing the account:
| Field | Description |
|---|---|
isOnBreak | Currently on a break |
breakStartedAt / breakEndsAt | Break window |
breakDurationDays | Planned duration |
scheduledBreakStartsAt | Future scheduled break |
autoReplyMessage | Auto-reply sent to anyone who messages during the break |
Memorialization
For accounts of deceased users:
| Field | Description |
|---|---|
isMemorialized / memorializedAt | Memorialization state |
memorializationStatus | Process status |
legacyContactId | Designated legacy contact |
allowTributes | Allow tribute posts |
allowLegacyContactPosts | Legacy contact can post |
showMemorialBanner | Show memorial banner on profile |
Profile queries
me returns the full authenticated user — use this on app startup to hydrate the current user context. user and userByUsername fetch public profile data for any account. searchUsers is the search-bar autocomplete. suggestedUsers returns personalized follow recommendations for the home screen.
query Me { me { id username bio profilePicture coverPhoto isVerified isPrivate twoFactorEnabled } }
query User($id: ID!) { user(id: $id) { id username } }
query UserByUsername($username: String!) { userByUsername(username: $username) { id username bio isVerified } }
query SearchUsers($query: String!, $limit: Int) { searchUsers(query: $query, limit: $limit) { id username profilePicture } }
query SuggestedUsers($limit: Int) { suggestedUsers(limit: $limit) { id username profilePicture } }
Username / email availability
These two queries are called in real time as the user types during sign-up or profile editing. isUsernameAvailable returns a message explaining why the username is taken (e.g. "Already in use").
query CheckUsername($username: String!) { isUsernameAvailable(username: $username) { available message } }
query CheckEmail($email: String!) { isEmailAvailable(email: $email) { available message } }
Stats & relationship
userStats returns counters (followers, following, close friends) and the relationship flags between the caller and the target — use this to render the follow/message button state on a profile.
relationshipStatus is a more detailed relationship object that also includes canMessage and canViewPosts — use it when rendering a profile to know whether to show the "Message" button and whether posts are accessible.
query UserStats($userId: ID!) {
userStats(userId: $userId) {
followersCount followingCount closeFriendsCount
isFollowing isFollower isCloseFriend isBlocked isBlockedBy hasPendingRequest
}
}
query RelationshipStatus($targetUserId: ID!) {
relationshipStatus(targetUserId: $targetUserId) {
isSelf isFollowing isFollower isBlocked isMuted isCloseFriend
hasPendingRequest isMutualFollowing canMessage canViewPosts
}
}
Social actions
followUser always creates the follow relationship immediately (it does not check isPrivate) — it's for callers that already know the target is public, e.g. SuggestedUsers.tsx's quick-follow button. sendFollowRequest is the private-account-aware entry point: internally it follows immediately if the target is public, or creates a pending request if private, and its response includes requestRequired: true in the latter case. PublicProfilePage.tsx's handleFollowClick picks between the two using the already-loaded user.isPrivate.
Note: the sendFollowRequest manager actually returns snake_case keys (request_required/request_id) while the schema declares non-null camelCase requestRequired — selecting it triggers a "Cannot return null for non-nullable field" error, so PublicProfilePage.tsx deliberately only selects success/message/requestId and relies on a relationshipStatus refetch to learn the real state.
unfollowUser removes the follow relationship. blockUser also unfollows if a follow existed. muteUser hides the user's content from your feed without removing the follow relationship. removeFollower removes someone from your followers without blocking them.
mutation FollowUser($userId: ID!) { followUser(userId: $userId) { success } }
mutation UnfollowUser($userId: ID!) { unfollowUser(userId: $userId) { success } }
mutation BlockUser($userId: ID!) { blockUser(userId: $userId) { success } }
mutation MuteUser($userId: ID!) { muteUser(userId: $userId) { success } }
mutation RemoveFollower($id: ID!) { removeFollower(followerId: $id) { success } }
# Follow requests (private accounts) - do not select requestRequired, see note above
mutation SendRequest($userId: ID!) { sendFollowRequest(userId: $userId) { success requestId } }
mutation AcceptRequest($id: ID!) { acceptFollowRequest(requestId: $id) { success } }
mutation RejectRequest($id: ID!) { rejectFollowRequest(requestId: $id) { success } }
mutation CancelRequest($id: ID!) { cancelFollowRequest(requestId: $id) { success } }
# Close Friends
mutation AddCloseFriend($userId: ID!) { addToCloseFriends(userId: $userId) { success } }
mutation RemoveCloseFriend($userId: ID!) { removeFromCloseFriends(userId: $userId) { success } }
# Favorites (reorderable, separate from following)
mutation AddFavorite($userId: ID!) { addToFavorites(userId: $userId) { favorite { position } } }
mutation RemoveFavorite($userId: ID!) { removeFromFavorites(userId: $userId) { success } }
mutation ReorderFavorites($ids: [ID!]!) { reorderFavorites(userIds: $ids) { success } }
Bulk operations
These mutations process a list of user IDs in a single API call. successCount and failedCount let the client display a summary toast without checking each result individually.
mutation BulkFollow($ids: [ID!]!) { bulkFollowUsers(userIds: $ids) { successCount failedCount } }
mutation BulkUnfollow($ids: [ID!]!) { bulkUnfollowUsers(userIds: $ids) { successCount } }
mutation BulkBlock($ids: [ID!]!) { bulkBlockUsers(userIds: $ids) { processedCount } }
mutation BulkRemoveFollowers($ids: [ID!]!) { bulkRemoveFollowers(followerIds: $ids) { processedCount } }
mutation BulkProcessRequests($ids: [ID!]!, $action: String!) {
bulkProcessFollowRequests(requestIds: $ids, action: $action) { processedCount }
}
mutation BulkMute($ids: [ID!]!) { bulkMuteUsers(userIds: $ids) { success message results { success failed } } }
Tag permissions
Control who can tag you in posts and whether tags require your approval before appearing. requireApproval: true means tags are queued for review. friendsOnly: true limits tagging to mutual followers. disabled: true prevents all tagging.
removeTag deletes an existing tag from one of your own posts and returns a Boolean. Approving/rejecting a pending tag (one queued because requireApproval is on) is a separate flow via pendingTagReviews/approvePostTag/rejectPostTag (below); a same-named approveTag/rejectTag pair was planned for this generic permissions system but was never wired up (updateTagPermissions and removeTag are the only mutations live here).
type TagPermissions {
canBeTagged: Boolean!
requireApproval: Boolean!
friendsOnly: Boolean!
disabled: Boolean!
}
query TagPermissions { tagPermissions { canBeTagged requireApproval friendsOnly disabled } }
# input is a JSON blob (no dedicated TagPermissionsInput type), shaped like TagPermissions
mutation UpdateTagPermissions($input: JSON!) {
updateTagPermissions(input: $input) { canBeTagged requireApproval friendsOnly disabled }
}
mutation RemoveTag($tagId: ID!) { removeTag(tagId: $tagId) }
Reviewing tags other people put you in (as opposed to editing your own permissions) is a separate pair of mutations exposed by tag-review.resolver.js:
query PendingTagReviews { pendingTagReviews { id user { id username } media { id } positionX positionY createdAt } }
mutation ApprovePostTag($mentionId: ID!) { approvePostTag(mentionId: $mentionId) }
mutation RejectPostTag($mentionId: ID!) { rejectPostTag(mentionId: $mentionId) }
Interests
Users select interests during onboarding (completeOnboarding) or later from their profile settings. Interests power interestSuggestions and other recommendation ranking. Unlike a separate taxonomy-backed system, interests here are just an array of free-form strings stored on the User (interestTags column, exposed as User.interests) — there's no Interest id/slug/icon type in this GraphQL flow. updateInterests replaces the viewer's entire selection at once (max 50 entries, trimmed and filtered by the resolver).
query MyInterests { me { interests } }
mutation UpdateInterests($interests: [String!]!) {
updateInterests(interests: $interests) { id interests }
}
query InterestSuggestions($limit: Int, $interests: [String!]) {
interestSuggestions(limit: $limit, interests: $interests) { id username profilePicture }
}
Social links
Users can link their profiles on other platforms. platform is a string like twitter, instagram, or website. These links are displayed on the user's own profile card (SocialLinksPage.tsx under Settings → Links) and on public profiles (PublicProfilePage.tsx via userSocialLinks).
mutation AddSocialLink($input: CreateSocialLinkInput!) {
addSocialLink(input: $input) { id platform url title clickCount }
}
mutation UpdateSocialLink($id: ID!, $input: UpdateSocialLinkInput!) {
updateSocialLink(id: $id, input: $input) { id platform url isActive visibility }
}
mutation DeleteSocialLink($id: ID!) { deleteSocialLink(id: $id) }
mutation ReorderSocialLinks($ids: [ID!]!) { reorderSocialLinks(ids: $ids) }
mutation TrackSocialLinkClick($id: ID!) { trackSocialLinkClick(id: $id) { id clickCount } }
query MySocialLinks { mySocialLinks { id platform url title displayOrder clickCount } }
query UserSocialLinks($userId: ID!) { userSocialLinks(userId: $userId) { id platform url title } }
query SocialLinkStats($userId: ID) { socialLinkStats(userId: $userId) { totalLinks totalClicks visibleLinks hiddenLinks platforms } }
Restriction appeals
If an account is suspended or banned (accountStatus), the user can submit one appeal against it via appealRestriction, provided they don't already have a pending one. myLatestAppeal returns the caller's most recent appeal of any status (or null if they've never appealed); getAppealStatus looks up one specific appeal by id, and the caller must own it.
mutation AppealRestriction($reason: String!) {
appealRestriction(reason: $reason) { id status submittedAt reason }
}
query GetAppealStatus($appealId: ID!) {
getAppealStatus(appealId: $appealId) { id status reason submittedAt reviewedAt decisionNotes }
}
query MyLatestAppeal { myLatestAppeal { id status reason submittedAt reviewedAt decisionNotes } }
Trending users
getTrendingUsers returns accounts with rapidly growing follower counts, ordered by trendingScore. trendingReason is a human-readable label like "Viral post" or "New verified account" — display it as a sub-label on the trending card. The manager itself returns a { trendingUsers, timeframe, category, pagination } wrapper, which the resolver unwraps to the [TrendingUser!]! list the schema declares.
query TrendingUsers($limit: Int, $offset: Int, $timeframe: String, $category: String) {
getTrendingUsers(limit: $limit, offset: $offset, timeframe: $timeframe, category: $category) {
user { id username profilePicture isVerified followersCount }
trendingScore growthRate trendingReason
}
}
Online users
getOnlineUsers returns accounts that are currently online — useful for a "Who's online" widget. lastSeen is the timestamp of the user's last heartbeat.
query OnlineUsers($limit: Int, $offset: Int) {
getOnlineUsers(limit: $limit, offset: $offset) {
user { id username profilePicture }
lastSeen isCurrentlyOnline activityStatus
}
}
Edit profile
updateProfile patches the authenticated user's profile. Only the fields passed in input are updated.
mutation UpdateProfile($input: UserProfileUpdateInput!) {
updateProfile(input: $input) { id username bio profilePicture coverPhoto }
}
Account management
mutation DeactivateAccount($password: String!) { deactivateAccount(password: $password) { success } }
mutation ReactivateAccount($password: String!) { reactivateAccount(password: $password) { success } }
mutation RequestDeletion($password: String!) { requestAccountDeletion(password: $password) { success } }
mutation CancelDeletion { cancelAccountDeletion { success } }
mutation DeleteAccount($password: String!) { deleteAccount(password: $password) { success } }
# Skips the grace period requestAccountDeletion schedules and deletes right away
mutation DeleteAccountImmediately($password: String!) { deleteAccountImmediately(password: $password) { success } }
Profile visitors
Every time a user opens someone else's public profile, the frontend fires recordProfileVisit in the background. The resolver never throws to the caller — a failure is swallowed and returns false — so a logging hiccup never blocks the profile from rendering. Visits are recorded on an append-only log (user_profile_view, modeled after user_post_view) and de-duplicated: a repeat visit from the same viewer within 60 seconds does not create a new row, and getMyViewers further de-dupes by viewer so someone who has visited many times only appears once, at their most recent visit time.
Visits to your own profile are never recorded. If the viewer has hideProfileVisits enabled (see Privacy settings), recordVisit no-ops silently — the visited user's list simply won't include that viewer, with no error surfaced to either side.
mutation RecordProfileVisit($targetUserId: ID!) { recordProfileVisit(targetUserId: $targetUserId) }
query MyProfileViewers($limit: Int, $offset: Int) {
myProfileViewers(limit: $limit, offset: $offset) {
viewer { id username profilePicture isVerified }
viewedAt
}
}
query MyProfileViewerCount { myProfileViewerCount }
mutation ClearMyProfileViewers { clearMyProfileViewers }