Skip to content

Discovery Module

Location: apps/api/src/modules/discovery/

Aggregate roots: ConnectionIntent

The Discovery context implements "Make Connections" (CON-01..03). Users declare a ConnectionIntent and are matched against compatible candidates based on shared interest count, intent compatibility, and optional geo/interest filters.


Intent Types

IntentCompatible With
networkingnetworking
friendsfriends
romanticromantic
mentormentee
menteementor
collabcollab

Prisma Model

connection_intents

FieldTypeNotes
userIdUUID PK FKOne per user
intentStringSee intent types above
noteString?Card text (shown in discovery feed)
isActiveBooleanfalse = hidden from candidates
updatedAtDateTime
createdAtDateTime

Key Methods

MethodDescription
upsertIntent(userId, dto)Set/update intent
getIntent(userId)Get current intent
getCandidates(userId, filters)Ranked candidates by shared interest count
getMentors(userId, filters)Mentors with expertise gap ≥ minExpertiseGap
getPeopleDirectory(userId, opts)Browseable user directory with isFollowing field
suggested(viewerId, limit)"People you may know" via pgvector user-centroid ANN
stream(viewerId, …)Unified #Discover stream across all content types

Candidate Ranking

getCandidates algorithm:

  1. Exclude: self, banned users, opted-out users (isActive=false).
  2. Filter by intentCompatibility(viewerIntent).
  3. Optional geo filter: city and/or country.
  4. Optional interest filter: interestSlug (must share that specific interest).
  5. Count shared interests via user_interests join.
  6. Sort: shared interest count DESC, then createdAt ASC.
  7. Paginate with cursor (userId-based).

Mentor Matching

getMentors finds users who:

  1. Have ConnectionIntent.intent = 'mentor' and isActive = true.
  2. Share at least one interest with the viewer.
  3. Mentor's UserInterest.expertise exceeds viewer's by minExpertiseGap (default 2, clamped 1..4).
  4. Optional: filter by a specific interestSlug.

HTTP Endpoints

All under /api/v1/discovery/. Require JwtAuthGuard.

MethodPathDescription
PUT/discovery/intentUpsert my intent { intent, note?, isActive? }
GET/discovery/intentGet my intent
GET/discovery/candidatesRanked discovery candidates
GET/discovery/mentorsMentor matches ?interestSlug&minExpertiseGap
GET/discovery/peoplePeople directory ?q&cursor&limit
GET/discovery/suggested"People you may know" (centroid ANN)
GET/discovery/talentTalent directory
GET/discovery/streamUnified #Discover stream (9 content types)

Query Parameters for /discovery/candidates

ParamTypeNotes
intentString?Override stored intent for query
interestSlugString?Filter by shared interest
countryString?Geo filter
cityString?Geo filter
minSharedInt?Min shared interests (default 1)
limitInt?Page size (default 20, max 50)
cursorString?userId cursor for pagination

People Directory

GET /discovery/people returns paginated users with:

  • id, username, firstName, lastName, profilePicUrl
  • isFollowing: boolean — whether the viewer follows this user
  • sharedInterests: number — count of shared interests

Used by the mobile "People" tab and contact picker.


Suggested People (centroid ANN)

GET /discovery/suggested powers "People you may know". It builds the viewer's preference centroid from their reactions + bookmarks and runs an approximate-nearest-neighbour search over user vectors (embeddings.similarUserIds). It gracefully degrades: when the viewer has no centroid yet (insufficient interaction history) or OPENAI_API_KEY is absent, the section simply hides — no error.


Semantic Feed Re-Rank

The home feed (in the Posts context) fetches a larger pool, scores it by engagement + depth alignment, then floats the top 40 % by semantic similarity (Math.ceil(limit * 0.4)) to the front via embeddings.reRankBySemantics. Embeddings use OpenAI text-embedding-3-small and the re-rank is a no-op when embeddings are disabled or the viewer has no preference vector — the engagement ranking stands on its own.


Unified #Discover Stream

GET /discovery/stream interleaves the Wave 3-6 content types into one chronological/ranked feed. The serializer emits 9 item types:

post · video · audio · event · debate · problem · course · project · offering

Each item carries actorId, interestId, createdAt, a sub badge, and (for media) a media URL. When video items are present, the serializer enriches them with a real engagement block — react / comment / repost counts plus the viewer's reacted / reposted state (computed via a group-by + viewer lookup, only when video items exist) — so feed video cards show live numbers with working toggles instead of a bare caption. Geo and trending filters apply across the stream.

Regulus — invite-only social-knowledge platform