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
| Intent | Compatible With |
|---|---|
networking | networking |
friends | friends |
romantic | romantic |
mentor | mentee |
mentee | mentor |
collab | collab |
Prisma Model
connection_intents
| Field | Type | Notes |
|---|---|---|
| userId | UUID PK FK | One per user |
| intent | String | See intent types above |
| note | String? | Card text (shown in discovery feed) |
| isActive | Boolean | false = hidden from candidates |
| updatedAt | DateTime | |
| createdAt | DateTime |
Key Methods
| Method | Description |
|---|---|
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 |
Candidate Ranking
getCandidates algorithm:
- Exclude: self, banned users, opted-out users (
isActive=false). - Filter by
intentCompatibility(viewerIntent). - Optional geo filter:
cityand/orcountry. - Optional interest filter:
interestSlug(must share that specific interest). - Count shared interests via
user_interestsjoin. - Sort: shared interest count DESC, then createdAt ASC.
- Paginate with cursor (userId-based).
Mentor Matching
getMentors finds users who:
- Have
ConnectionIntent.intent = 'mentor'andisActive = true. - Share at least one interest with the viewer.
- Mentor's
UserInterest.expertiseexceeds viewer's byminExpertiseGap(default 2, clamped 1..4). - Optional: filter by a specific
interestSlug.
HTTP Endpoints
All under /api/v1/discovery/. Require JwtAuthGuard.
| Method | Path | Description |
|---|---|---|
| PUT | /discovery/intent | Upsert my intent { intent, note?, isActive? } |
| GET | /discovery/intent | Get my intent |
| GET | /discovery/candidates | Ranked discovery candidates |
| GET | /discovery/mentors | Mentor matches ?interestSlug&minExpertiseGap |
| GET | /discovery/people | People directory ?q&cursor&limit |
Query Parameters for /discovery/candidates
| Param | Type | Notes |
|---|---|---|
intent | String? | Override stored intent for query |
interestSlug | String? | Filter by shared interest |
country | String? | Geo filter |
city | String? | Geo filter |
minShared | Int? | Min shared interests (default 1) |
limit | Int? | Page size (default 20, max 50) |
cursor | String? | userId cursor for pagination |
People Directory
GET /discovery/people returns paginated users with:
id,username,firstName,lastName,profilePicUrlisFollowing: boolean— whether the viewer follows this usersharedInterests: number— count of shared interests
Used by the mobile "People" tab and contact picker.