Skip to main content

User Profile — Technical Reference

← Back to User Profile

Where this lives

Backend

Frontend

Technical implementation checklist

  • me — resolver wired in user-profile.resolver.js; rendered by ProfilePage.tsx
  • updateProfile — mutation wired; EditProfilePage.tsx's handleSave calls it on the "Save changes" button (this doc previously claimed no onClick handler existed — corrected)
  • userByUsername — resolver wired; rendered by PublicProfilePage.tsx
  • isVerified badge — rendered next to the username whenever isVerified is true, in both ProfilePage.tsx and PublicProfilePage.tsx
  • Social links (addSocialLink / updateSocialLink / deleteSocialLink / mySocialLinks / userSocialLinks) — now live end to end: social-link.resolver.js + social-link.type.js expose the schema over social-link.manager.js / social-link.access-service.js, and SocialLinksPage.tsx / PublicProfilePage.tsx consume it (this doc previously claimed no GraphQL exposure existed — corrected)
  • updatePrivacySettings — mutation wired; AccountPrivacyPage.tsx calls it via useMutation with 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 calls handleFollowClick, which invokes them (this doc previously claimed no onClick handler existed — corrected)
  • Profile visitors — PublicProfilePage.tsx fires recordProfileVisit on mount; ProfileViewersPage.tsx lists myProfileViewers; the hideProfileVisits toggle on AccountPrivacyPage.tsx makes recordVisit a no-op for that user's own visits to other profiles

User model fields

Identity

FieldDescription
usernameUnique username
firstName / lastNameFull name
bioProfile bio
profilePictureProfile picture URL
coverPhotoCover photo URL
dateOfBirth / genderDemographic info
accountTypeAccount type (personal, creator, etc.)

Account status

FieldDescription
accountStatusCurrent account state
isVerified / verificationStatusVerification badge
verificationCategoryBadge category (e.g. creator, business)
isPrivatePrivate account (requires follower approval)
isEmailVerified / isPhoneVerifiedContact verification
isOnboardingCompleted / onboardingStepOnboarding progress

Activity

FieldDescription
isOnlineCurrently online
lastActiveAtLast seen timestamp
lastPostAtDate of last post
engagementScoreCalculated engagement score
locale / timezoneRegional settings

Security

FieldDescription
twoFactorEnabled2FA enabled
securityNotificationsEnabledSecurity alerts active
isTermsConditionsAcceptedT&C accepted

Restrictions & suspension

FieldDescription
suspensionCount / suspendedAtSuspension history
isRestricted / restrictionTypesActive restrictions
tagApprovalRequiredMust approve tags before they appear
autoRemoveTagsAuto-removes tags from content

Break mode

Allows temporarily pausing the account:

FieldDescription
isOnBreakCurrently on a break
breakStartedAt / breakEndsAtBreak window
breakDurationDaysPlanned duration
scheduledBreakStartsAtFuture scheduled break
autoReplyMessageAuto-reply sent to anyone who messages during the break

Memorialization

For accounts of deceased users:

FieldDescription
isMemorialized / memorializedAtMemorialization state
memorializationStatusProcess status
legacyContactIdDesignated legacy contact
allowTributesAllow tribute posts
allowLegacyContactPostsLegacy contact can post
showMemorialBannerShow 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 }
}

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 } }

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 }