Nodes Subgraph
The Nodes subgraph is the canonical owner of user profiles on the Ngwenya platform. It extends the base identity provided by the auth service (which manages login credentials) into fully-featured consumer profiles with handles, social connections, privacy controls, and cross-service metadata.
Port:
3009ยท Federation: Apollo v2 ยท Database: MongoDB (users,follows,collections,user_attributes,blocked_userscollections)
Architecture
apps/nodes/src/
โโโ actors/user/ โ User entity, mutations, federation, privacy resolvers
โ โโโ user.entity.ts โ Federated User type (@key "id", @extends)
โ โโโ user-mutation.resolver.ts โ Profile updates, handle claims, preferences
โ โโโ user-federation.resolver.ts โ __resolveReference for cross-subgraph resolution
โ โโโ content-visibility.resolver.ts โ contentDisplayName, contentAvatarUrl
โ โโโ age-verification.resolver.ts โ Age verification status + self-declaration
โ โโโ support-proxy.resolver.ts โ Pseudo-anonymous aliases, contact sharing
โ โโโ admin-users.resolver.ts โ Tower-scoped user listing + search
โ โโโ admin-users.service.ts โ Paginated user queries (admin)
โ โโโ account-change-hook.controller.ts โ TCP listener for account lifecycle events
โ โโโ cookie-consent.service.ts โ GDPR cookie preferences
โ โโโ data-export.service.ts โ GDPR data export (JSON download)
โโโ attributes/ โ Key-value metadata per user (cross-service)
โโโ blocked-users/ โ User block/unblock system
โโโ collections/ โ Wishlists and curated collections
โโโ follows/ โ Social graph (follow/unfollow)
โโโ membership/ โ Organization membership federation
โโโ murchaser-dashboard/ โ Buyer-side order aggregation
โโโ utils/ โ Shared utilities
All providers are registered in a single NodesModule. The directory structure is for code organization only.
Core Entity: User
The User entity federates from the auth service using @key(fields: "id") with @extends. The auth service owns the id field; Nodes owns everything else.
type User @key(fields: "id") @extends {
id: ID! @external
# Profile (owned by Nodes)
displayName: String
handle: String # v|handle sigil
bio: String
avatarUrl: String
location: String
website: String
company: String
handleChangedAt: DateTime
handleHistory: [HandleHistoryEntry!]!
# Privacy-Aware Display
contentDisplayName: String! # Computed from privacy settings
contentAvatarUrl: String # null when anonymous
contentVisibilityStatus: ContentVisibility!
# Age Verification
isAgeVerified: Boolean!
ageVerificationStatus: AgeVerificationStatus!
# Support Anonymity
supportAlias: String # e.g. "Customer-A7B3C"
supportContactEmail: String # Only visible if consumer explicitly shared
supportContactPhone: String
# Preferences
preferences: UserPreferences!
attributes: [UserAttribute!]!
}
Profile Auto-Creation
When auth creates a new user, it emits a user_created TCP event. The AppController in Nodes listens for this event and auto-creates the corresponding User document in MongoDB:
@EventPattern('user_created')
async handleUserCreated(data: { userId: string; email?: string }) {
await this.userModel.create({ userId: data.userId, email: data.email });
}
If a profile query arrives before the event (race condition), the resolvers use lazy initialization โ creating the profile on-demand during the first mutation.
Handle System (`v|` sigil)
Users claim a globally unique handle with the v| sigil prefix (e.g., v|sarah). The handle becomes the user's public URL slug: mallnline.com/u/sarah.
Rules
| Rule | Details |
|---|---|
| Format | Lowercase alphanumeric + hyphens, 2โ30 characters |
| Uniqueness | Unique across the user namespace. Cross-namespace check with checkHandleAvailability (malets) recommended |
| Reserved | 35+ reserved words including platform routes (admin, support, lobby), u-products (ucart, uchat), and brand names (mallnline, ngwenya) |
| Cooldown | Handle changes limited to once every 30 days |
| History | All previous handles are stored in handleHistory[] |
| Permanence | Handles can be changed (with cooldown), but the old handle is not released โ it remains in the history |
API
# Check availability
query { checkUserHandleAvailability(handle: "sarah") } # โ true/false
# Claim or change handle
mutation { updateProfile(input: { handle: "sarah" }) { id handle handleChangedAt } }
Social Graph
Follows
The follow system implements a unidirectional social graph stored in the follows collection. Users can follow three target types:
| Target Type | Sigil | Example |
|---|---|---|
MALET |
`m | ` |
USER |
`v | ` |
ORGANIZATION |
`o | ` |
# Follow
mutation { followEntity(input: { targetId: "...", targetType: MALET }) { id } }
# Unfollow
mutation { unfollowEntity(targetId: "...") }
# Query followers / following
query { followers(targetId: "...", targetType: MALET) { edges { node { id } } } }
query { following(userId: "...") { edges { node { id targetType } } } }
query { isFollowing(userId: "...", targetId: "...") } # โ Boolean
Indexing: Compound unique index on (followerId, targetId) prevents duplicates. Secondary index on (followerId, targetType) powers "all Malets I follow" queries.
Collections (Wishlists)
Users create named collections to organize saved items (see Social Wishlist Collaboration for real-time synchronization and sharing architecture):
# CRUD
mutation { createCollection(input: { name: "Summer Wishlist" }) { id } }
mutation { addToCollection(collectionId: "...", itemId: "...", itemType: PRODUCT) { id } }
mutation { removeFromCollection(collectionId: "...", itemId: "...") { id } }
# Query
query { myCollections { id name itemCount items { itemId itemType } } }
Content Visibility & Privacy
The ContentVisibilityResolver computes what name and avatar to display on reviews, comments, and community content based on the user's privacy preferences:
| Setting | contentDisplayName |
contentAvatarUrl |
|---|---|---|
FULL_NAME |
User's display name | User's avatar |
FIRST_NAME_ONLY |
First name or initial | User's avatar |
ANONYMOUS |
"Anonymous" | null (default avatar shown) |
This resolver is used by the community, reviews, Q&A, and blog comment subgraphs via federation. When those subgraphs resolve a User entity, the contentDisplayName field automatically applies the privacy mask.
Support Anonymity
The SupportProxyResolver provides pseudo-anonymous identities for support interactions:
supportAliasโ A randomly generated alias (e.g.,Customer-A7B3C) scoped to a Malet/Organization. Stable across interactions with the same seller.supportContactEmail/supportContactPhoneโ Only populated if the consumer explicitly shared contact info on a specific support ticket (viaShareContactWithIssuemutation).
This ensures Malet Owners can respond to support requests without seeing the buyer's real identity unless the buyer opts in.
Age Verification
Self-declaration age verification for age-restricted products and services:
| Status | Meaning |
|---|---|
UNVERIFIED |
No age declaration submitted |
VERIFIED |
User declared they meet the minimum age threshold |
EXPIRED |
Verification period lapsed (annual re-verification) |
DECLINED |
User declined to verify |
mutation { declareAge(dateOfBirth: "1990-01-15") { isAgeVerified ageVerificationStatus } }
The platform never exposes the user's actual date of birth. Only the boolean
isAgeVerifiedand theageVerificationStatusenum are accessible in the GraphQL schema.
Blocked Users
Users can block other users, hiding them from search results and preventing messaging:
mutation { blockUser(targetUserId: "...") { id } }
mutation { unblockUser(targetUserId: "...") }
query { blockedUsers { targetUserId blockedAt } }
query { isBlocked(targetUserId: "...") } # โ Boolean
Cross-Service Dependencies
| Service | Integration |
|---|---|
| auth | user_created event โ auto-profile creation |
| community | User.contentDisplayName / contentAvatarUrl via federation |
| alerts | TCP consumer for user_created (welcome notification) |
| organizations | Membership federation (org.members โ User) |
| malets | Malet owner display via User federation |
| gateway | x-user-id header โ identity resolution |
GraphQL API Reference
Queries
# Profile
user(id: ID!): User
checkUserHandleAvailability(handle: String!): Boolean!
# Social Graph
followers(targetId: ID!, targetType: FollowTargetType!, limit: Int): FollowConnection!
following(userId: ID!, limit: Int): FollowConnection!
isFollowing(userId: ID!, targetId: ID!): Boolean!
followerCount(targetId: ID!): Int!
followingCount(userId: ID!): Int!
# Collections
myCollections: [Collection!]!
collection(id: ID!): Collection
# Admin (Tower)
adminUsers(page: Int, limit: Int, search: String): UserConnection!
Mutations
# Profile
updateProfile(input: UserUpdateDTO!): User!
updatePreferences(input: UserPreferences!): User!
updatePrivacySettings(input: PrivacyPreferences!): User!
toggleProfileVisibility(visibility: VisibilityPrivacy!): User!
# Push Tokens
registerPushToken(token: String!): User!
unregisterPushToken(token: String!): User!
# Social Graph
followEntity(input: CreateFollowInput!): Follow!
unfollowEntity(targetId: ID!): Boolean!
# Collections
createCollection(input: CreateCollectionInput!): Collection!
addToCollection(collectionId: ID!, itemId: ID!, itemType: CollectionItemType!): Collection!
removeFromCollection(collectionId: ID!, itemId: ID!): Collection!
deleteCollection(id: ID!): Boolean!
# Support
shareContactWithIssue(issueId: ID!): Boolean!
revokeContactFromIssue(issueId: ID!): Boolean!
# Age Verification
declareAge(dateOfBirth: String!): User!
# Blocking
blockUser(targetUserId: ID!): UserBlock!
unblockUser(targetUserId: ID!): Boolean!
Testing
| Suite | Tests | Coverage |
|---|---|---|
| User Mutations | 8 | Handle claims, cooldown, reserved words, profile updates |
| Follows | 6 | Follow/unfollow, duplicates, query counts |
| Collections | 7 | CRUD, item management, ownership |
| Content Visibility | 4 | Privacy mask computation |
| Support Proxy | 4 | Alias generation, contact sharing |
| Admin Users | 3 | Pagination, search |
| E2E | 5 | User creation, events, follows, admin |
| Total | ~37 | โ |
Related
- Handle System โ Cross-namespace handle reservation and sigil conventions
- Social Graph Architecture โ Follow System, feed algorithms, and aggregation
- Privacy & Security APIs โ Cookie consent, GDPR export, session management
- Support Ticket Anonymity โ Pseudo-anonymous aliases and contact sharing architecture
- User Identity Resolution โ How
x-user-idmaps to a profile across subgraphs - Wishlists & Collections โ Collection CRUD and item management API
- Social Wishlist Collaboration โ Architecture for collaborative custom lists and data synchronization.