User Feedback — Technical Reference
Where this lives
Backend
apps/backend/graphql/types/user-feedback.type.js— theUserFeedbacktype,SubmitFeedbackInput, and the queries/mutations described belowapps/backend/graphql/resolvers/user-feedback.resolver.js— resolvers backing that schema, delegating into the managerapps/backend/managers/user-managers/user-feedback.manager.js— business logic (submitFeedback,submitBugReport,submitFeatureRequest,getUserFeedback,getFeedback,getPublicFeedback,upvoteFeedback,downvoteFeedback,getMyVotesFor,getAllFeedback,getFeedbackStats,updateStatus,setPriority,markAsResolved,getMostUpvoted, etc.). Only a subset is wired into GraphQL — see the checklist below.apps/backend/data-access-services/user/user-feedback.access-service.js— DB layer for theuser_feedbacktableapps/backend/data-access-services/user/user-feedback-vote.access-service.js— DB layer for theuser_feedback_votededup tableapps/backend/database/models/UserFeedback.js/ migration20220122175043-create-user-feedback.js— theuser_feedbacktable (title,description,feedback_type,category,platform,status,priority,admin_response,upvotes,downvotes, etc.)apps/backend/database/models/UserFeedbackVote.js/ migration20260718140000-create-user-feedback-vote.js— theuser_feedback_votetable (one row per user per feedback item, unique onfeedback_id+user_id) plus theupvotes/downvotescolumns onuser_feedback
Frontend
apps/frontend-nextjs/src/page-components/settings/FeedbackPage.tsx(routed atapps/frontend-nextjs/src/app/settings/feedback/page.tsx, linked fromSettings → Send feedback) — the submit form, the "Your submissions" history (myFeedback), and the "Community feature requests" votable board (publicFeedback+upvoteFeedback/downvoteFeedback). GraphQL operations are declared inline in the component (no generated hooks yet, same pattern asSocialLinksPage.tsx).apps/frontend-admin/src/app/feedback/page.tsx+FeedbackContent.tsx— the admin triage queue (/feedback, requiresMANAGE_FEEDBACK). See User Feedback (admin) for the full admin GraphQL surface and permission details.
Technical implementation checklist
-
submitFeedback— wired:SubmitFeedbackInput→ resolver →user-feedback.manager.js#submitFeedback; used byFeedbackPage.tsx -
myFeedback— wired, authenticated; used byFeedbackPage.tsx's "Your submissions" list -
feedbackById— wired, authenticated, owner-only (resolves tonullif the caller doesn't own the item) -
publicFeedback— wired; votable board, defaults tofeedbackType: feature_request; used byFeedbackPage.tsx's "Community feature requests" -
upvoteFeedback/downvoteFeedback— wired, deduped per user viauser_feedback_vote; used byFeedbackPage.tsx -
submitBugReport/submitFeatureRequestshorthand — exist only as manager methods; not exposed as GraphQL mutations -
getMostUpvoted— exists only as a manager method; not exposed as a GraphQL query (thepublicFeedbackquery already orders by upvotes and serves as the public leaderboard) - Admin review (
adminGetFeedback,adminGetFeedbackStats,adminUpdateFeedbackStatus,adminSetFeedbackPriority,adminRespondToFeedback) — wired on the admin schema, gated byMANAGE_FEEDBACK, with a real/feedbackadmin frontend. See User Feedback (admin).markAsResolved/getMostUpvoted/getByStatus/getByPriorityremain manager-only (unused by any resolver - the latter two are dead code left over from beforegetAllFeedback/getAllgrew filter support).
Feedback types
| Type | Description |
|---|---|
bug | Something is broken or not working as expected |
feature_request | Request for a new feature |
general | General feedback |
complaint | Complaint about the platform or another user |
suggestion | Improvement idea |
Submitting feedback
submitFeedback creates a new feedback item. feedbackType determines how it's routed. title is the one-line summary; description is the full text. The response includes the initial status (pending).
mutation SubmitFeedback($input: SubmitFeedbackInput!) {
submitFeedback(input: $input) {
id feedbackType title status createdAt
}
}
SubmitFeedbackInput fields: feedbackType (required — bug | feature_request | general | complaint | suggestion), title (required), description (required), category (optional, e.g. ui_ux | performance | feature | content | security | privacy | other; defaults to other), platform (optional; defaults to web), satisfactionRating (optional 1-5 int).
The manager also exposes submitBugReport(userId, title, description) / submitFeatureRequest(userId, title, description) convenience wrappers that set feedbackType automatically, but they are not exposed as GraphQL mutations — the client sets feedbackType directly in SubmitFeedbackInput instead (see FeedbackPage.tsx's type selector).
User queries
myFeedback shows the current user's submission history with live status updates, including any adminResponse.
query MyFeedback($limit: Int, $offset: Int) {
myFeedback(limit: $limit, offset: $offset) {
id feedbackType title description status priority upvotes adminResponse createdAt
}
}
feedbackById fetches a single item by id — the caller must own it, otherwise it resolves to null.
query FeedbackById($id: ID!) {
feedbackById(id: $id) {
id feedbackType title description status priority adminResponse createdAt
}
}
Voting
Community members can signal which feature requests or bug reports matter most. upvoteFeedback / downvoteFeedback are deduped per user via the user_feedback_vote table (one row per user per feedback item): voting again toggles the vote off, and voting the opposite way switches it. The denormalized upvotes/downvotes counts live on user_feedback, and UserFeedback.myVote reflects the caller's current vote ('up' | 'down' | null).
publicFeedback is the public, votable board — it orders by upvotes descending, defaults to feedbackType: feature_request, and accepts 'all' to browse every type. There is no separate mostUpvotedFeedback query; publicFeedback's ordering already serves as the leaderboard.
mutation UpvoteFeedback($feedbackId: ID!) { upvoteFeedback(feedbackId: $feedbackId) { id upvotes downvotes myVote } }
mutation DownvoteFeedback($feedbackId: ID!) { downvoteFeedback(feedbackId: $feedbackId) { id upvotes downvotes myVote } }
query PublicFeedback($feedbackType: String, $limit: Int, $offset: Int) {
publicFeedback(feedbackType: $feedbackType, limit: $limit, offset: $offset) {
id title description feedbackType status upvotes downvotes myVote createdAt
user { id username }
}
}
Status lifecycle
pending → in_review → in_progress → resolved
↘ rejected
↘ duplicate
This is the set validated by the manager's updateStatus method, exposed as adminUpdateFeedbackStatus on the admin schema (see "Admin management" below).
Admin management
graphql/resolvers/admin/user-feedback-admin.resolver.js wires up the admin-facing operations, gated by context.admin + the MANAGE_FEEDBACK permission (see User Feedback (admin) for the full checklist, permission details, and GraphQL reference):
adminGetFeedback→user-feedback.manager.js#getAllFeedback— list feedback with optionalstatus/priority/feedbackTypefilters, paginatedadminUpdateFeedbackStatus→updateStatus— moves an item through the lifecycle aboveadminSetFeedbackPriority→setPriority— assigns urgency (low/medium/high/critical)adminRespondToFeedback→ the newrespondToFeedbackmanager method — setsadminResponse,reviewedBy(the calling admin's id), andreviewedAttogether in one actionadminGetFeedbackStats→getFeedbackStats— breakdown by type, status, and priority, plus vote totals
markAsResolved (sets status: 'resolved', resolvedAt, and a free-text resolution field that isn't actually a column on UserFeedback - a pre-existing no-op) has no resolver and isn't used by the admin UI, which drives status entirely through adminUpdateFeedbackStatus.
adminResponse is exposed in myFeedback / feedbackById / publicFeedback as well as the admin schema, so a response written via adminRespondToFeedback shows up in FeedbackPage.tsx immediately.
Priority levels
| Priority | Description |
|---|---|
low | Minor issue, no urgency |
medium | Default priority |
high | Significant impact |
critical | Blocking / urgent |