Onboarding — Technical Reference
Where this lives
Backend
apps/backend/graphql/resolvers/user-social.resolver.js—completeOnboarding(interests)mutation (enforces the 5-follow minimum, setsisOnboardingCompleted)apps/backend/graphql/resolvers/user-search-discovery.resolver.js—interestSuggestions(limit, interests)queryapps/backend/data-access-services/user/user.access-service.js—getUsersByInterests()(interest overlap),getSuggestedUsers()(broad top-up)apps/backend/data-access-services/user/user-follow.access-service.js—getFollowingCount()used by the 5-follow gate
Frontend
apps/frontend-nextjs/src/page-components/OnboardingPage.tsx— the two-step flow (interests → follow ≥5) + search box + logoutapps/frontend-nextjs/src/app/onboarding/page.tsx— routeapps/frontend-nextjs/src/components/ProtectedRoute.tsx— the gate (redirects unfinished users to/onboarding, renders nothing meanwhile)apps/frontend-nextjs/src/components/Login.tsx— post-login redirect sends unfinished users straight to/onboardingapps/frontend-nextjs/src/lib/interests.ts— shared interest catalog (slugs + labels)
Technical implementation checklist
-
completeOnboarding(interests: [String!]): User!— requiresgetFollowingCount(userId) >= 5server-side, setsisOnboardingCompleted+onboardingCompletedAt, stores interests -
interestSuggestions(limit, interests): [User!]!— uses passed interests (live during onboarding) or the viewer's stored interests; tops up withgetSuggestedUsersso the list is never short - Persistent gate —
ProtectedRouteredirects whenuser.isOnboardingCompleted === false(flag comes from the login/register payload; re-login re-triggers it) -
isOnboardingCompletedadded to the login / 2FA-verify / OAuth (Firebase, Apple, phone, passkey) user selections inLogin.tsx; theRegistermutation doesn't request the field, soLogin.tsxdefaults it tofalsethere (new accounts always need onboarding)
GraphQL
type Query {
interestSuggestions(limit: Int, interests: [String!]): [User!]!
}
type Mutation {
completeOnboarding(interests: [String!]): User!
}
type User {
isOnboardingCompleted: Boolean! # self-only
# ...
}
Notes
- Interests are stored on
user.interests(text[]) — the Sequelize model attribute isinterestTags(fieldinterests) to avoid colliding with the existingInterestmany-to-many association namedinterests. - Reuses the existing
is_onboarding_completedcolumn, so there is no migration for the gate itself; theuser.interestscolumn ships in20260721120000-add-interests-to-user.