@okrlinkhub/page-feedback
v1.1.0
Published
A page feedback component for Convex.
Readme
Convex Page Feedback
Collect versioned page feedback per user and URL inside an isolated Convex component.
The component keeps exactly one active feedback thread per
userId + normalizedUrl, stores every update as a new version, and lets each
feedback thread host:
- linear discussion comments
- emoji reactions on comments
It also supports a shared page-purpose layer per normalizedUrl with:
- ordered
objectivesdescribing what the page should achieve - lightweight
indicatorsattached to each objective - discussion threads attached to each objective
It also includes a singleton settings record for optional bug-report and
improvement-request URLs controlled by the consumer app.
Found a bug? Feature request? File it here.
Installation
Create a convex.config.ts file in your app's convex/ folder and install the
component by calling use:
// convex/convex.config.ts
import { defineApp } from 'convex/server'
import pageFeedback from '@okrlinkhub/page-feedback/convex.config.js'
const app = defineApp()
app.use(pageFeedback)
export default appThen run component codegen in your project:
npx convex dev --typecheck-componentsUsage
import { components } from './_generated/api'
import { exposeApi } from '@okrlinkhub/page-feedback'
export const {
getMyFeedback,
upsertFeedback,
setFeedbackSolved,
listFeedbackVersions,
listLatestFeedbackForUrl,
listObjectivesForUrl,
upsertObjective,
listIndicatorsForObjective,
upsertIndicator,
listComments,
listObjectiveComments,
addComment,
addObjectiveComment,
editComment,
deleteComment,
getCommentReactions,
toggleReaction,
getSettings,
setSettings,
} = exposeApi(components.pageFeedback, {
auth: async (ctx) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new Error('Unauthorized')
}
return identity.tokenIdentifier
},
adminAuth: async (ctx) => {
const identity = await ctx.auth.getUserIdentity()
if (!identity) {
throw new Error('Admin access required')
}
},
})See more example usage in example.ts.
Data model
The component stores data in eight tables:
feedbackThreads: latest feedback snapshot for eachuserId + normalizedUrl, includingisSolvedfeedbackVersions: append-only history for each feedback threadfeedbackComments: linear discussion messages attached to a feedback threadfeedbackReactions: emoji reactions attached to a feedback commentpageObjectives: shared objectives for a normalized page URLobjectiveIndicators: ordered indicators for a single objectiveobjectiveComments: discussion messages attached to a page objectivesettings: singleton configuration document keyed internally asglobal
The URL is normalized inside both the component and the client wrapper by taking
everything before ?.
Public component API
The installed component exposes these public functions:
lib.upsertFeedback({ userId, url, rating, note })lib.setFeedbackSolved({ userId, threadId, isSolved })lib.getMyFeedback({ userId, url })lib.listFeedbackVersions({ userId, url, limit? })lib.listLatestFeedbackForUrl({ url, limit? })lib.listObjectivesForUrl({ url })lib.upsertObjective({ objectiveId?, url, description, status, order })lib.listIndicatorsForObjective({ objectiveId })lib.upsertIndicator({ indicatorId?, objectiveId, description, order })lib.listComments({ threadId, limit?, currentUserId? })lib.listObjectiveComments({ objectiveId, limit? })lib.addComment({ userId, threadId, body })lib.addObjectiveComment({ userId, objectiveId, body })lib.editComment({ userId, commentId, body })lib.deleteComment({ userId, commentId })lib.getCommentReactions({ commentId, currentUserId? })lib.toggleReaction({ userId, commentId, emoji })lib.getSettings({})lib.setSettings({ bugReportUrl?, improvementRequestUrl? })
rating is constrained to integers from 1 to 3.
isSolved is a thread-level state, not a versioned feedback field. Updating the
rating or note still creates a new row in feedbackVersions, while changing
isSolved only updates the latest thread snapshot. For compatibility with
existing installations, older threads without isSolved are treated as
unsolved until they are updated.
This release intentionally does not include mentions or realtime typing. Those can be layered in later without coupling the component to a specific user directory or notification architecture.
Best practices
This component follows the official Convex guidance for components from Authoring Components:
- authentication remains in the consumer app, not inside the component
- the component owns its own tables and persistence boundary
- public functions define both argument and return validators
- external app identifiers such as
userIdare passed explicitly across the component boundary
HTTP Routes
You can register a read-only HTTP endpoint for page feedback:
import { httpRouter } from 'convex/server'
import { registerRoutes } from '@okrlinkhub/page-feedback'
import { components } from './_generated/api'
const http = httpRouter()
registerRoutes(http, components.pageFeedback, {
pathPrefix: '/feedback',
})
export default httpThis exposes GET /feedback/latest?url=... and returns the latest feedback
entries for the requested normalized URL. See
http.ts for a complete example.
Run the example:
npm i
npm run dev