Developer Docs

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 a QuestionComment as 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:

  1. Who can accept? โ€” Only the Question Author (the Visitor who asked the question). Future: Malet Owners via cross-subgraph ownership verification.
  2. What happens on accept? โ€” The acceptedAnswerId is set on the Question, status transitions to ANSWERED, and a question_answered event is emitted via AlertsClient.
  3. Can it be changed? โ€” Yes. The author can unacceptAnswer (reverts to PENDING), then acceptAnswer on a different reply.
  4. 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 + @handle inline 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
  • ActorBadge for 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