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
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
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
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: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.