Unified Q&A โ Threaded Conversations
The Q&A system provides a StackOverflow-style threaded conversation model for Visitors to ask questions about Products, Services, or Malets, and for the community to collaboratively answer them. Questions and their replies implement the unified Comment interface, making them first-class citizens in the platform's conversation infrastructure.
Architecture Overview
Q&A is built on the Comment interface pattern โ the same foundation used by Issues, Discussions, and Reviews. This ensures consistency across all community conversation types.
| Component | Service | Purpose |
|---|---|---|
Question |
community subgraph |
Top-level question entity implementing Comment |
QuestionComment |
community subgraph |
Threaded reply on a Question (also a Comment) |
ActorBadge |
Frontend component | Interactive participant identity with hover card |
ContentFilterService |
community subgraph |
Spam, phishing, and repetition detection |
AlertsClient |
community โ alerts |
Notification dispatch on answer acceptance |
Data Flow
โโโโโโโโโโโโโโโโโโโโโโโ
โ Visitor asks โ
โ a Question โ
โโโโโโโโโโฌโโโโโโโโโโโโโ
โผ
โโโโโโโโโโโโโโโโโโโโโโโ ContentFilter
โ Question entity โ โโโ spam/phishing check
โ (Comment interface)โ
โ status: PENDING โ
โโโโโโโโโโฌโโโโโโโโโโโโโ
โ community replies
โผ
โโโโโโโโโโโโโโโโโโโโโโโ
โ QuestionComment[] โ โ threaded replies
โ (Comment interface)โ
โโโโโโโโโโฌโโโโโโโโโโโโโ
โ author accepts answer
โผ
โโโโโโโโโโโโโโโโโโโโโโโ TCP emit
โ acceptedAnswerId โ โโโโโโโโโโโโโโโบ Alerts Service
โ status: ANSWERED โ โ question_answered
โโโโโโโโโโโโโโโโโโโโโโโ โ Email/Push notification
Entity Schema
Question
The Question entity extends the Comment interface and is the root of a threaded conversation.
| Field | Type | Description |
|---|---|---|
id |
String! |
MongoDB document ID |
questionID |
String! |
Human-readable ID (e.g. Q-abc123) |
body |
String! |
Question text (Markdown) |
authorId |
String! |
User ID of the Visitor who asked |
subjectId |
String! |
Product, Service, or Malet ID |
subjectType |
SubjectType! |
PRODUCT, SERVICE, MALET, or ORGANIZATION |
maletId |
String! |
Parent Malet ID โ always set regardless of subjectType |
status |
QuestionStatus! |
PENDING, ANSWERED, or CLOSED |
acceptedAnswerId |
String |
ID of the accepted QuestionComment (nullable) |
isLiked |
Boolean! (resolved) |
Whether the current Visitor has upvoted this question |
likeCount |
Int! (resolved) |
Total upvote count |
comments |
QuestionCommentConnection |
Cursor-paginated threaded replies |
createdAt |
DateTime! |
Auto-generated timestamp |
updatedAt |
DateTime! |
Auto-updated timestamp |
QuestionComment
Individual threaded replies on a Question.
| Field | Type | Description |
|---|---|---|
id |
String! |
MongoDB document ID |
questionCommentID |
String! |
Human-readable ID (e.g. QC-def456) |
name |
String! |
Reply body text |
questionId |
String! |
Parent Question reference |
createdBy |
String! |
User ID of the reply author |
isLiked |
Boolean! (resolved) |
Whether the current Visitor has upvoted this reply |
likeCount |
Int! (resolved) |
Total upvote count on this reply |
createdAt |
DateTime! |
Auto-generated timestamp |
Status Lifecycle
โโโโโโโโโโโโ acceptAnswer() โโโโโโโโโโโโ
โ PENDING โ โโโโโโโโโโโโโโโโโโโบ โ ANSWERED โ
โโโโโโโโโโโโ โโโโโโโโโโโโ
โ โ
โ updateQuestionStatus() โ
โผ โผ
โโโโโโโโโโโโ โโโโโโโโโโโโ
โ CLOSED โ โโโโโโโโโโโโโโโโโ โ CLOSED โ
โโโโโโโโโโโโ unacceptAnswer() โโโโโโโโโโโโ
reverts to PENDING
PENDINGโ Default status on creation. Awaiting community replies.ANSWEREDโ The Question Author (or Malet Owner) has accepted aQuestionCommentas the best answer.CLOSEDโ Administratively closed by platform staff. No new replies accepted.
GraphQL API
Queries
# Fetch questions for a product, service, or malet
query GetQuestions($subjectId: String!, $first: Int!) {
questions(
filter: { subjectId: { eq: $subjectId } }
paging: { first: $first }
sorting: [{ field: createdAt, direction: DESC }]
) {
edges {
node {
id
questionID
body
authorId
subjectId
subjectType
status
isLiked
likeCount
acceptedAnswerId
comments(sorting: [{ field: createdAt, direction: ASC }]) {
edges {
node {
id
questionCommentID
name
createdBy
createdAt
isLiked
likeCount
}
}
totalCount
}
createdAt
updatedAt
}
}
pageInfo { hasNextPage endCursor }
totalCount
}
}
Mutations
# Create a new question
mutation CreateQuestion($input: CreateOneQuestionInput!) {
createOneQuestion(input: $input) {
id
questionID
body
authorId
status
createdAt
}
}
# Reply to a question
mutation CreateQuestionComment($input: CreateOneQuestionCommentInput!) {
createOneQuestionComment(input: $input) {
id
questionCommentID
name
createdBy
createdAt
}
}
# Accept a reply as the best answer (Question Author only)
mutation AcceptAnswer($questionId: ID!, $commentId: ID!) {
acceptAnswer(questionId: $questionId, commentId: $commentId) {
id
status
acceptedAnswerId
}
}
# Unaccept a previously accepted answer
mutation UnacceptAnswer($questionId: ID!) {
unacceptAnswer(questionId: $questionId) {
id
status
acceptedAnswerId
}
}
# Upvote a question
mutation ToggleQuestionLike($input: ToggleLikeInput!) {
toggleLike(input: $input) { isLiked likeCount }
}
Mutation Input Examples
// CreateQuestion
{
"input": {
"question": {
"body": "What material is this jacket made of?",
"subjectId": "product-abc-123",
"subjectType": "PRODUCT"
}
}
}
// CreateQuestionComment (reply)
{
"input": {
"questionComment": {
"name": "It's made of 100% organic cotton.",
"questionId": "680abc..."
}
}
}
Accepted Answer Model
The accepted answer pattern follows StackOverflow conventions:
- Who can accept? โ Only the Question Author (the Visitor who asked the question). Future: Malet Owners via cross-subgraph ownership verification.
- What happens on accept? โ The
acceptedAnswerIdis set on the Question, status transitions toANSWERED, and aquestion_answeredevent is emitted viaAlertsClient. - Can it be changed? โ Yes. The author can
unacceptAnswer(reverts toPENDING), thenacceptAnsweron a different reply. - Notifications โ The reply author receives an email/push notification when their answer is accepted.
Authorization Flow
// QuestionResolver.acceptAnswer()
1. Verify question exists โ NotFoundException
2. Verify actor === question.authorId โ ForbiddenException
3. Verify comment exists โ NotFoundException
4. Update question: { acceptedAnswerId, status: ANSWERED }
5. Emit question_answered alert โ AlertsClient
Content Filtering
All new questions pass through ContentFilterService before persisting, which checks for:
| Pattern | Detection |
|---|---|
| Spam keywords | `\b(spam |
| Excessive URL density | More than 3 URLs in a single question |
| Character repetition abuse | Repeated characters exceeding 10x |
Flagged questions have their status set to CLOSED and trigger a question_flagged alert for moderation review.
Frontend Components
ActorBadge
A reusable identity component for rendering conversation participants across all community surfaces.
| Prop | Type | Description |
|---|---|---|
userId |
string |
User ID to resolve |
profiles |
ProfileMap |
Map of resolved profiles for batch display |
authorAssociation |
string? |
OWNER, MEMBER, COLLABORATOR, CONTRIBUTOR, etc. |
isPrivate |
boolean? |
If true, renders as "Anonymous" with no interactive UI |
Features:
- Avatar initials +
@handleinline display - 300ms delayed hover popover with full profile card
- Follow and Message (uChat DM) buttons in popover
- Privacy-aware: private accounts show "Anonymous" with no interactive features
- Association badges color-coded by role:
| Association | Color | Badge |
|---|---|---|
| Owner | Blue | OWNER |
| Member | Green | MEMBER |
| Collaborator | Amber | COLLAB |
| Contributor | Purple | CONTRIB |
| First Timer | Pink | FIRST |
QuestionCard
Renders an individual question with its threaded reply list, upvote button, and accept/unaccept controls.
| Prop | Type | Description |
|---|---|---|
question |
QuestionNode |
Question data with nested comments |
isOwner |
boolean |
Enables accept/unaccept buttons |
profiles |
ProfileMap |
Resolved participant profiles |
Key interactions:
- Optimistic upvote with bounce animation
- Collapsible reply thread with count indicator
- Accepted answer highlighted with green border + badge
- Reply form available to all authenticated Visitors
ActorBadgefor question author and each reply author
QASection
Container component that fetches questions, renders stats, and provides a submission form.
| Prop | Type | Description |
|---|---|---|
maletId |
string |
Parent Malet ID (used for community-page queries) |
subjectId |
string |
Product, Service, or Malet ID |
subjectType |
string |
PRODUCT, SERVICE, or MALET |
isOwner |
boolean |
Enables owner-specific UI controls |
Module Structure
apps/community/src/comments/
โโโ comment.interface.ts # Comment base interface (ID, body, authorID, etc.)
โโโ question/
โ โโโ question.entity.ts # Question extends Comment, @CursorConnection
โ โโโ question.module.ts # Module wiring (imports QuestionCommentModule)
โ โโโ question.resolver.ts # acceptAnswer, unacceptAnswer, isLiked, likeCount
โ โโโ question.resolver.spec.ts # 12 unit tests
โ โโโ question-status.enum.ts # PENDING | ANSWERED | CLOSED
โ โโโ content-filter.service.ts # Spam/phishing detection
โ โโโ dto/
โ โโโ create-question.input.ts # body, subjectId, subjectType + auto-gen questionID
โโโ question-comment/
โ โโโ question-comment.entity.ts # QuestionComment entity
โ โโโ question-comment.module.ts # CRUD module with cursor pagination
โ โโโ question-comment.resolver.ts # isLiked, likeCount resolved fields
โ โโโ dto/
โ โ โโโ create-question-comment.input.ts # name, questionId + auto-gen ID
โ โโโ types.ts # Type exports
src/lib/ (frontend)
โโโ components/
โ โโโ ActorBadge.svelte # Identity badge with hover popover
โ โโโ QuestionCard.svelte # Threaded question card
โ โโโ QASection.svelte # Q&A container with stats/form
โโโ queries/community.ts # GraphQL queries, mutations, and types
โโโ utils/
โโโ qaUtils.ts # Sorting, formatting, stats
โโโ profileResolver.ts # Batch profile resolution
Testing
| Suite | Tests | Coverage |
|---|---|---|
question.resolver.spec.ts |
12 | acceptAnswer, unacceptAnswer, status, isLiked |
content-filter.service.spec.ts |
5 | Spam, URLs, repetition, clean content |
qaUtils.test.ts |
27 | Sorting, formatting, stats, date utilities |
Running Tests
# Backend (community subgraph)
npx jest --roots apps/community --testPathPattern='question/' --no-coverage
# Frontend
npm run test -- --run qaUtils
Related
- Community Features โ Reviews, helpful votes, product variants, and the broader community surface that Q&A lives within
- Identity Security & Cross-Subject Q&A โ Sigil enforcement,
maletIdarchitecture, and the subject-link sidebar - Community Orchestration โ Issue/Discussion assignment workflows and notification dispatch patterns shared by Q&A
- Alerts Resilience & Delivery Tracking โ DLQ retry and multi-channel delivery for
question_answerednotifications - uChat Client SDK โ DM integration available via ActorBadge hover popover