Skip to main content
Feedback is the one module here whose rows are written by your users rather than by you. Three surfaces sit over them, and what each one may carry is the design.

The board your users see

position takes inline (default), center, bottom-right, bottom-center or bottom-left — the same five the survey takes, and one vocabulary on purpose: a product that puts its survey in the corner and its board in the centre is describing one layout, not two. Inline the board expands inside the card, under the heading it belongs to. The three floating values leave only the button where you put it, which is what makes them worth having: a header or a menu has room for a button and not for a list of ideas with a form under it. Only center dims the page; the corners float over a page that stays readable and clickable, and Escape closes all three. In a Next app, import it from @userkit/nextjs — the same component, and in proxy mode the board read, the vote and the submission are forwarded by the handlers, with the session in an httpOnly cookie on your own origin. The board sits behind the verified-contact gate, for a reason the changelog’s and the survey’s are not: it is not what the answer reveals, it is what the write does. Submitting puts a stranger’s words on a page you publish under your own name, and a vote is a row your roadmap ranking is built from — both are assertions anybody who knows an external_id could otherwise make from a browser. Anonymous sessions pass that gate, as they do everywhere, and are then refused by the two writes with identity_required: the widget mints one per page load, so a board that counted them would be counting reloads. Reading is open to them, which is what the public document already shows anyway. The board tells this person which posts they voted for. It never tells them what those voters pay. A person may post a few requests a day; past that the API answers 429, which is “later” rather than “never”.

Five statuses

open, planned, in_progress, shipped, declined — and they travel in that order, which is why the vocabulary is returned in the response rather than left for a client to hard-code. Every value that is not open names a different answer you gave. There is no sixth value meaning “duplicate”: that is merged_into_id, and merging is an act rather than a status.

The public roadmap

The ninth cacheable document, and the only one whose rows were written by your users rather than by you — which is why what it carries is deliberately narrow: the title, the body, the status and a plain vote count. Not who asked, which is your data about your user. Not the revenue behind the votes, which never leaves the panel at all. body is plain text written by one of your users. Never render it as markup. It is the one column on this plane that somebody outside your staff can put onto a page you publish. Every board read is bounded at 500 posts rather than paginated. That bound is a product statement as much as a technical one — a board with five hundred live posts has a triage problem rather than a pagination problem — and it has to be one number across the three reads, because the ranking is computed over the set the list returned.

The weight

A post’s weight is the summed monthly value of the active subscriptions of the distinct customers behind its voters — so a team of five is worth its revenue once, not five votes. Annual prices are divided by 12, one-time prices never enter it, and a trial enters when it ends. It exists on this endpoint and nowhere else. Not on the public roadmap, not on the contact’s own board, not on a webhook. It is a separate query that only the staff list makes, so a public response cannot acquire a weight by somebody adding a field to a shared projection. Your users may know how many people want a thing; they may not know what those people pay. The currency is an argument, never a guess. There is no exchange table here, so summing minor units across currencies produces a number that is neither — asking for the weighted order without naming a currency is a 400 rather than a default. And the answer reports how many of the customers behind each post pay in a different currency or in nothing at all, because a weight of zero next to four other_currency_customers says something completely different from a weight of zero next to four free_customers. A bare number would say neither, and a reassurance is exactly what stops somebody checking.

Merging duplicates

The votes move to the survivor with ON CONFLICT DO NOTHING, so somebody who voted for both counts once. merged_into_id records where the post went, and each moved vote records which post it arrived from. There is no un-merge endpoint, and there does not need to be: both halves of the provenance stayed, so a wrong merge is repairable by hand. feedback_post.merged is deliverable precisely because it changes the vote count of a post nobody edited — a mirror of your board that heard only “created” and “status changed” would keep the duplicate forever and be wrong about the total on the survivor.

Telling the voters it shipped

Aim a changelog post at the feedback post instead of at a segment:
The audience is a join, not a copied recipient list: somebody who votes after you publish receives it, somebody who withdraws stops. Like any targeted post, it stays out of the public changelog document.

What leaves the building

feedback_post.created, feedback_post.status_changed, feedback_post.merged and feedback_vote.cast are all deliverable as webhooks. status_changed carries both statuses, so you act on the transition rather than diffing two payloads — the bus is unordered, so “the previous one” is not something a consumer can reconstruct. Setting a status a post already has announces nothing. feedback_vote.cast carries the post and the contact and never the weight.

Permissions

feedback:read is held by owner, admin and member — triage is support work, and the member seat is the support agent. feedback:write — answering, merging, filing on somebody’s behalf — is owner and admin. It is the one place in this module set where the read is split off from the write. The changelog, checklists and surveys share one engagement:manage, because operating what your users are shown is one act; a feedback board is something your support desk reads all day and rarely changes.