User Feedback
Admins can review, triage, and respond to user-submitted feedback (bug reports, feature requests, complaints, suggestions, and general feedback) from /feedback. Users already had a full submission + voting surface (submitFeedback, publicFeedback, upvoteFeedback/downvoteFeedback — see User Feedback), but there was previously zero admin-side way to see, prioritize, or respond to any of it.
This was built almost entirely on top of existing infrastructure: database/models/UserFeedback.js already had every column needed for triage (status, priority, adminResponse, reviewedBy, reviewedAt), and managers/user-managers/user-feedback.manager.js already had most of the business logic (getAllFeedback, updateStatus, setPriority, getFeedbackStats). What was missing was the admin GraphQL surface itself, a respondToFeedback action that sets adminResponse/reviewedBy/reviewedAt together, and filtering support on getAll.
Implementation checklist
Admin triage (/feedback, requires MANAGE_FEEDBACK)
- List feedback with optional
status/priority/feedbackTypefilters, newest first (adminGetFeedback) - Aggregate counts by type/status/priority plus total up/downvotes (
adminGetFeedbackStats) - Update an item's triage status —
pending | in_review | in_progress | resolved | rejected | duplicate(adminUpdateFeedbackStatus) - Set an item's priority —
low | medium | high | critical(adminSetFeedbackPriority) - Write an admin response, which also records who reviewed the item and when in one action (
adminRespondToFeedback)
Two real gaps closed along the way
data-access-services/user/user-feedback.access-service.js#getAll(options)only supported{limit, offset}— no filtering. It now accepts optionalstatus/priority/feedbackTypeand merges them into the Sequelizewhere. The manager's oldergetByStatus/getByPrioritymethods called access-service methods (getByStatus/getByPriority) that never existed — anis not a functionbug if either was ever actually invoked. They're left as unreferenced dead code (nothing calls them) rather than three near-duplicate list methods;getAllFeedback/getAllwith filters replaces their intended purpose.UserFeedback.reviewedByis meant to hold an admin's id, but the original migration (20220122175043-create-user-feedback.js) pointed its foreign key atuser(a copy-paste from the other columns on that table, which correctly reference the feedback submitter). Migration20260802140000-fix-user-feedback-reviewed-by-references-admin-user.jscorrects the FK to referenceadmin_user(the same distinctionappeal.reviewed_byalready got right), nulling out the handful of existing rows whosereviewed_byheld a strayuserid first (pre-existing seed data — never a real admin action, since no admin review surface existed until now). The model association (UserFeedback.js) was corrected to match (belongsTo(models.AdminUser, ...)).
A deliberate schema tradeoff
status/priority/adminResponse/reviewedBy/reviewedAt were already declared on the shared UserFeedback GraphQL type (the full schema is assembled once before being split into client/admin scopes, so a type declared once resolves in both). That means a regular user's myFeedback/feedbackById queries can also read those fields on their own feedback — not a data leak (both queries are already ownership-checked), and arguably useful (a user can see the admin's response to their own report). See the comment at the top of graphql/types/user-feedback.type.js.
Where this lives
Backend
apps/backend/graphql/types/user-feedback.type.js— sharedUserFeedbacktype (client + admin)apps/backend/graphql/types/admin/user-feedback-admin.type.js— admin schema (FeedbackStats,adminGetFeedback,adminGetFeedbackStats,adminUpdateFeedbackStatus,adminSetFeedbackPriority,adminRespondToFeedback)apps/backend/graphql/resolvers/user-feedback.resolver.js/graphql/resolvers/admin/user-feedback-admin.resolver.jsapps/backend/managers/user-managers/user-feedback.manager.js— one manager backs both the user-facing submission/voting flow and the admin triage surfaceapps/backend/data-access-services/user/user-feedback.access-service.js—getAllnow supports status/priority/feedbackType filtersapps/backend/database/migrations/20260802140000-fix-user-feedback-reviewed-by-references-admin-user.js— corrects thereviewed_byforeign key fromusertoadmin_user
Frontend — apps/frontend-admin/src/app/feedback/page.tsx + FeedbackContent.tsx. Nav entry gated by the MANAGE_FEEDBACK permission (or super_admin), same pattern as the MANAGE_APP_VERSIONS-gated App Version Control section.
Permission — MANAGE_FEEDBACK, added to the real permission catalog in admin-user.manager.js#getAdminPermissions (category content_moderation, alongside MODERATE_CONTENT/REMOVE_CONTENT).
GraphQL reference
# Admin (requires MANAGE_FEEDBACK)
query AdminGetFeedback($status: String, $priority: String, $feedbackType: String, $limit: Int, $offset: Int) {
adminGetFeedback(status: $status, priority: $priority, feedbackType: $feedbackType, limit: $limit, offset: $offset) {
id title description feedbackType status priority adminResponse
reviewedBy reviewedAt upvotes downvotes createdAt
user { id username profilePicture }
}
}
query AdminGetFeedbackStats {
adminGetFeedbackStats { total byType byStatus byPriority totalUpvotes totalDownvotes }
}
mutation AdminUpdateFeedbackStatus($id: ID!, $status: String!) {
adminUpdateFeedbackStatus(id: $id, status: $status) { id status }
}
mutation AdminSetFeedbackPriority($id: ID!, $priority: String!) {
adminSetFeedbackPriority(id: $id, priority: $priority) { id priority }
}
mutation AdminRespondToFeedback($id: ID!, $response: String!) {
adminRespondToFeedback(id: $id, response: $response) { id adminResponse reviewedBy reviewedAt }
}
# Client (authenticated - unchanged by this work)
query MyFeedback { myFeedback { id title status priority adminResponse } }
mutation SubmitFeedback($input: SubmitFeedbackInput!) { submitFeedback(input: $input) { id } }