Search & Discovery — Technical Reference
Where this lives
Backend
apps/backend/graphql/resolvers/user-search-discovery.resolver.js— resolvessearchUsers,suggestedUsers,getTrendingUsers,getRecommendedUsers,getSearchStats,getDiscoveryInsights,refreshRecommendations,hideFromSuggestions,reportSearchResultapps/backend/graphql/resolvers/user-recommendations.resolver.js— resolvesusersYouMayKnow/similarUsers/dismissSuggestion, matchinggraphql/types/user-recommendations.type.js. This resolver file exists and its return shape ({ peopleYouMayKnow, limit, offset, total, hasMore }/{ similarUsers, ... }) matches the schema exactly — confirmed working, not a stub.apps/backend/graphql/types/user-presence.type.js— also declaresgetRecommendedUsers,getDiscoveryInsights,getSearchStats(duplicated field declarations across two type files pointing at the same manager methods), plusgetBulkOperationStatus/getBulkOperationHistory/getBulkOperationLimits/cancelBulkOperationapps/backend/graphql/types/user.type.js— also declaresUser.isFollowing: BooleanandUser.followContext: MutualFollowContext({ count, sample: User }), resolved per-field ingraphql/resolvers/user-fields.resolver.js—followContextcomputes social-proof for suggestion cards (people the viewer follows who also follow this user), null for anonymous/self callersapps/backend/graphql/types/user.type.js— declaresbulkFollowUsers(userIds: [ID!]!)/bulkUnfollowUsers(userIds: [ID!]!)returningBulkOperationResponse(success,message,successCount,failedCount,total,errors: [BulkError!])apps/backend/graphql/types/social-actions.type.js— declaresbulkBlockUsers(userIds: [ID!]!),bulkRemoveFollowers(followerIds: [ID!]!),bulkProcessFollowRequests(requestIds: [ID!]!, action: String!)returningBulkOperationResult(success,message,processedCount,failedCount,operationId)apps/backend/graphql/resolvers/user-bulk-operations.resolver.js— resolves the bulk mutations above plusgetBulkOperationLimits/getBulkOperationHistory/getBulkOperationStatusapps/backend/managers/user-managers/search-discovery.manager.js— ranking/search logic behind recommendationsapps/backend/managers/user-managers/bulk-operations.manager.js— bulk-action business logic
Frontend
apps/frontend-nextjs/src/components/SuggestedUsers.tsx—suggestedUsers(selectsisFollowingandfollowContext { count sample { id username firstName profilePicture } }for the "Followed by" social-proof label); its "See all" link goes to/discover-peopleapps/frontend-nextjs/src/components/discovery/RecommendationsSection.tsx—usersYouMayKnow,similarUsers,getTrendingUsers,getRecommendedUsers,nearbyUsers,dismissSuggestion; rendered insidepage-components/ExplorePage.tsxabove the discover grid. Fixed this session: the category tabs (Recommended / You may know / Similar / Trending / Nearby) used to be wired but hidden —activeTabwas hardcoded to'recommended', so onlygetRecommendedUsersever rendered. There's now a realrole="tablist"tab bar (TABSarray,activeTabclient state viasetActiveTab) letting the user switch between all five, each backed by its own query (skip: activeTab !== '<tab>'on eachuseQuery) so only the active tab's query actually runs.apps/frontend-nextjs/src/page-components/DiscoverPeoplePage.tsx— full-page version of the same recommendation family (getRecommendedUsers,similarUsers,usersYouMayKnow,getTrendingUsers,nearbyUsers), each rendered as its own always-visible section instead of a single tabbed strip; routed atapp/discover-people/page.tsx(/discover-people, authenticated-only)apps/frontend-nextjs/src/page-components/settings/BulkFollowerActionsPage.tsx—bulkFollowUsers/bulkUnfollowUsers/bulkBlockUsers/bulkRemoveFollowers/bulkProcessFollowRequests, at Settings → Bulk actions (/settings/bulk-follower-actions)apps/frontend-nextjs/src/page-components/settings/AccountPrivacyPage.tsx— Fixed this session:hideFromSuggestions, at Settings → Privacy → Show in Explore — see the note belowsearchUsersis also called from:components/chat/ConversationList.tsx(start a new conversation),components/CreatePostModal.tsx(tag people),page-components/settings/StoryLivePage.tsx(hide story from).app/search/page.tsxnow branches on auth state: authenticated users getExplorePage(with a newhideRecommendationsprop that suppressesRecommendationsSection, since results replace it), logged-out visitors getpage-components/PublicSearchPage.tsx, a read-onlysearchUsers+searchHashtagsresults page.
A pre-existing backend bug fixed while wiring this page
user-bulk-operations.resolver.js's mutations were written to destructure { input } from their GraphQL args and read input.userIds / input.requestIds / input.action. But the actual schema (user.type.js / social-actions.type.js) declares every bulk mutation with flat arguments (userIds: [ID!]!, followerIds: [ID!]!, requestIds: [ID!]!, action: String!) — there is no input wrapper anywhere in the schema. Calling any of these mutations as originally written would throw Cannot read properties of undefined (reading 'userIds') immediately. Separately, the manager's return shape ({ success, message, results: { success, failed, skipped, errors } }) didn't match either BulkOperationResponse or BulkOperationResult's flat, non-nullable fields (successCount!, failedCount!, total!, processedCount!), which would have thrown a "Cannot return null for non-nullable field" GraphQL error. Both were fixed directly in the resolver (see mapToBulkOperationResponse / mapToBulkOperationResult helpers in the file) as part of building the frontend for this page — not a pre-existing "done" feature as earlier notes assumed.
getBulkOperationHistory and getBulkOperationStatus call userManager.getBulkOperationHistory / userManager.getBulkOperationStatus, but neither method exists in bulk-operations.manager.js (only getBulkOperationLimits does) — there is no operation-tracking persistence layer behind these two fields or behind cancelBulkOperation. They're left as-is (still throwing a clean GraphQL error if ever called) but the frontend deliberately does not call them; bulk actions run synchronously and report their result inline instead of via a polled progress bar.
hideFromSuggestions is now wired to the "Show in Explore" toggle
Fixed this session: the schema (user-presence.type.js) has declared the single mutation hideFromSuggestions(enabled: Boolean!): Boolean! for a while, resolved (user-search-discovery.resolver.js) into search-discovery.manager.js#hideFromSuggestions/#showInSuggestions depending on the enabled flag — but no frontend screen called it. AccountPrivacyPage.tsx's Settings → Privacy → Show in Explore toggle previously persisted through the generic updatePrivacySettings mutation, which flips the same underlying appear_in_suggestions flag but doesn't invalidate the caller's cached recommendation results. The toggle's handler (handleToggleShowInExplore) now calls the dedicated hideFromSuggestions mutation instead (enabled: !nextValue, since the mutation's enabled means "hide me" while the toggle's own value means "show me"), so switching it off also invalidates the cache immediately instead of waiting for it to expire naturally.
Technical implementation checklist
-
searchUsers— wired; three separate call sites, Explore's search bar, andPublicSearchPage.tsxfor logged-out visitors -
interestSuggestions(limit, interests)+updateInterests(interests)— interest-based suggestions;user.intereststext[] (model attrinterestTags),getUsersByInterestsoverlap match (migration20260721120000) -
suggestedUsers— wired;SuggestedUsers.tsx, now withisFollowing+followContextsocial-proof data -
usersYouMayKnow/similarUsers— wired;RecommendationsSection.tsxandDiscoverPeoplePage.tsx -
getRecommendedUsers/getTrendingUsers/nearbyUsers— wired;RecommendationsSection.tsx(all five tabs — Recommended/You may know/Similar/Trending/Nearby — now render via a real tab bar, fixed this session, previously onlygetRecommendedUsersrendered sinceactiveTabwas hardcoded) andDiscoverPeoplePage.tsx(all render) -
dismissSuggestion(userId)— wired;RecommendationsSection.tsx's per-card dismiss (X) button -
hideFromSuggestions— Fixed this session:AccountPrivacyPage.tsx's Settings → Privacy → Show in Explore toggle now calls the dedicatedhideFromSuggestionsmutation instead of the genericupdatePrivacySettingsfield — see note above -
bulkFollowUsers/bulkUnfollowUsers/bulkBlockUsers/bulkRemoveFollowers/bulkProcessFollowRequests— wired;BulkFollowerActionsPage.tsx(backend resolver bug fixed as part of this work — see above) -
getBulkOperationHistory/getBulkOperationStatus/cancelBulkOperation— now implemented: abulk_operationtable + model + access-service; each bulk action records real counts/status, status/history read the real rows, and cancel returns false for already-finished (synchronous) operations
Search
query SearchUsers($query: String!, $limit: Int, $offset: Int, $filters: JSON) {
searchUsers(query: $query, limit: $limit, offset: $offset, filters: $filters) {
id username profilePicture isVerified
}
}
query SuggestedUsers($limit: Int, $offset: Int, $category: String) {
suggestedUsers(limit: $limit, offset: $offset, category: $category) {
id username profilePicture
}
}
Recommendations
query UsersYouMayKnow($limit: Int, $offset: Int) {
usersYouMayKnow(limit: $limit, offset: $offset) {
peopleYouMayKnow {
user { id username }
matchScore
reasons { type mutualConnectionsCount confidence }
}
total
hasMore
}
}
query SimilarUsers($limit: Int, $offset: Int) {
similarUsers(limit: $limit, offset: $offset) {
similarUsers { user { id username } similarityScore sharedInterestScore activitySimilarity }
total
hasMore
}
}
query GetTrendingUsers($limit: Int, $timeframe: String) {
getTrendingUsers(limit: $limit, timeframe: $timeframe) {
user { username }
trendingReason
}
}
query GetRecommendedUsers($limit: Int, $offset: Int, $algorithm: String) {
getRecommendedUsers(limit: $limit, offset: $offset, algorithm: $algorithm) { username }
}
query NearbyUsers($radiusKm: Float, $limit: Int) {
nearbyUsers(radiusKm: $radiusKm, limit: $limit) { user { id username } city distanceKm }
}
mutation DismissSuggestion($userId: ID!) { dismissSuggestion(userId: $userId) }
mutation HideFromSuggestions($enabled: Boolean!) { hideFromSuggestions(enabled: $enabled) }
dismissSuggestion(userId) (declared in user-recommendations.type.js, resolved by user-recommendations.resolver.js into search-discovery.manager.js#dismissSuggestion) removes that user from the caller's "people you may know" suggestions going forward; RecommendationsSection.tsx also hides the card client-side immediately, before the mutation resolves.
hideFromSuggestions(enabled: true) is a self-toggle — it hides the caller from other users' suggestion lists (persists appear_in_suggestions: false); it does not hide one specific other user from the caller's own suggestions, and it has no effect on searchUsers (direct search isn't filtered by this flag today). Fixed this session: AccountPrivacyPage.tsx's Settings → Privacy → Show in Explore toggle now calls this mutation (see the note above) — previously no frontend screen did.
suggestedUsers and suggestion cards elsewhere can also select User.isFollowing and User.followContext { count sample { id username } } — followContext returns social-proof ("Followed by ...") data: how many accounts the viewer follows also follow this user, plus one sample user for the label. Both fields resolve to null for anonymous callers or when viewing your own profile.
Bulk relationship actions
mutation BulkFollowUsers($userIds: [ID!]!) {
bulkFollowUsers(userIds: $userIds) { success message successCount failedCount total errors { userId reason } }
}
mutation BulkUnfollowUsers($userIds: [ID!]!) {
bulkUnfollowUsers(userIds: $userIds) { success message successCount failedCount total errors { userId reason } }
}
mutation BulkBlockUsers($userIds: [ID!]!) {
bulkBlockUsers(userIds: $userIds) { success message processedCount failedCount }
}
mutation BulkRemoveFollowers($followerIds: [ID!]!) {
bulkRemoveFollowers(followerIds: $followerIds) { success message processedCount failedCount }
}
mutation BulkProcessFollowRequests($requestIds: [ID!]!, $action: String!) {
bulkProcessFollowRequests(requestIds: $requestIds, action: $action) { success message processedCount failedCount }
}
action for bulkProcessFollowRequests is 'accept' or 'reject', applied to every id in requestIds — the manager itself supports mixed per-request actions ({requestId, action}[]), but the GraphQL field only exposes the single-action-for-many-ids shape, so the resolver expands requestIds into that array server-side.