Feedback Plugin
Feature requests, voting, a public roadmap, and linked release notes
Best for
Teams adding a public feedback board and roadmap to an existing React application.
Collect feature requests, rank them with votes, and publish delivery updates.

BTST supplies
- Feature requests with category and status filters and one vote per user and post
- A public Planned, In Progress, and Shipped roadmap
- A release changelog linked to shipped requests
- SSR-aware routes, authorization operations, hooks, and ejectable pages
You supply
- A BTST database adapter with isolated transaction support
- Authenticated user identities and administrator authorization rules
- The application shell and deployment
You own and customize
Requests, votes, and release notes stay in your database. Your application supplies identities, access rules, and an adapter with isolated transactions.
Compatibility and dependencies
Maintained: Next.js 15+ App Router, React Router v7, TanStack Start.
Requires: A BTST database adapter with isolated transaction support.
External services: None required.
From registration to result
A semantic workflow, not a setup shortcut
- 1Collect requests
Users submit ideas and vote once per request.
- 2Plan delivery
Administrators move requests through Planned, In Progress, and Shipped.
- 3Publish releases
Link shipped requests to changelog entries.
Installation
pnpm add @btst/plugin-feedback@1.1.0Register feedbackBackendPlugin() from @btst/plugin-feedback/api in the backend
stack and feedbackClientPlugin() from @btst/plugin-feedback/client in the
client stack, both under the feedback key. Configure shared API/site locations
and the query client through createClientStack, then pass the resolved stack
to StackProvider as described in the installation guide.
@import "@btst/plugin-feedback/css";The CLI accepts --plugins feedback. Writes require an adapter with isolated
transactions. The generated Prisma, Drizzle, and Kysely configurations enable
transactions; the memory and MongoDB scaffolds are rejected. The browser-only
playground does not supply authenticated voter identities or administration rules.
Authorization
Use feedbackPermissions from @btst/plugin-feedback/permissions in the
application's shared authorization contract:
import { feedbackPermissions } from "@btst/plugin-feedback/permissions"
import { defineAuthorization } from "@btst/stack/authorization"
import { z } from "zod"
export const authorization = defineAuthorization({
identity: z.object({ id: z.string(), role: z.enum(["user", "admin"]) }),
permissions: [feedbackPermissions],
rules: ({ feedback }) => [
feedback.post.read.allow(),
feedback.post.create.when(({ identity }) => identity !== null),
feedback.post.vote.when(({ identity }) => identity !== null),
feedback.post.updateStatus.when(({ identity }) => identity?.role === "admin"),
feedback.post.linkKanbanTask.when(({ identity }) => identity?.role === "admin"),
feedback.changelog.read.allow(),
feedback.changelog.create.when(({ identity }) => identity?.role === "admin"),
],
})Connect this contract to the server and client authentication adapters. The server resolves identity before invoking operations; API callers cannot choose another author's or voter's identity. Public read access and authenticated write access are separate permissions. Status and changelog management require explicit administrator rules. Without server authentication, protected writes fail closed.
Trusted application operations are available on stack.trusted.feedback. Use
stack.forRequest(request).operations.feedback for request-bound authorization.
Routes
Routes are relative to the configured site base path (for example /pages).
| Route | Behavior |
|---|---|
/feedback | Feature requests, category/status filters, submission, and votes |
/feedback/[slug] | Request details, voting, and administrator status changes |
/feedback/roadmap | Planned, In Progress, and Shipped columns |
/feedback/changelog | Release entries with links to shipped requests |
All four routes prefetch through SSR loaders. Pass the request's identity as
feedbackClientPlugin({ identityPartition: requestIdentity }) when constructing
the server client stack so personalized vote state uses the same cache partition
as the hydrated browser. Forward request headers through the shared API runtime.
Data and operations
feedbackPost stores a title, slug, body, category, author, status, timestamps,
and vote count. Statuses are under_review, planned, in_progress, shipped,
and closed. New requests start under review. feedbackVote permits one vote
per user and post; voting toggles that record and updates the count in one
isolated transaction. Concurrent votes cannot overwrite another user's count.
changelogEntry stores a title, body, publication date, and linked post IDs.
Only shipped requests can be linked when creating an entry. The release feed
returns linked requests and their detail URLs.
| Operation | Input |
|---|---|
listPosts | { category?, status?, limit?, offset? } |
getPost | { slug } |
createPost | { title, body, category? } |
toggleVote | { id } |
updatePostStatus | { id, status } |
getRoadmap | {} |
listChangelog | { limit?, offset? } |
createChangelogEntry | { title, body, postIds, publishedAt? } |
HTTP routes live under the configured API base path plus /feedback: GET/POST /posts, GET /posts/:slug, POST /posts/:id/vote, PUT /posts/:id/status,
GET /roadmap, and GET/POST /changelog.
Hooks and customization
feedbackBackendPlugin({ hooks: { onAfterUpdatePostStatus(post, previousStatus) } }) runs the server hook after an authorized status update commits. Both arguments describe the actual transition; hook errors propagate after the write. Use getFeedbackVoterIds(adapter, postId) from the server API for a trusted voter lookup. The optional Notifications lifecycle bridge notifies voters only when the status changes.
Import data hooks from @btst/plugin-feedback/client/hooks: useFeedbackPosts,
useFeedbackPost, useFeedbackRoadmap, useFeedbackChangelog,
useCreateFeedbackPost, useToggleFeedbackVote, useUpdateFeedbackStatus, and
useCreateChangelogEntry. Query keys are exported through
createFeedbackQueryKeys in @btst/plugin-feedback/query-keys.
The components entry exports FeedbackPageComponent,
FeedbackPostPageComponent, FeedbackRoadmapPageComponent,
FeedbackChangelogPageComponent, and FeedbackPostCard. Types are available from
@btst/plugin-feedback/types.
The postBottomSlot override renders application content beneath a detail post,
for example an application-owned discussion integration:
<StackProvider
stack={stack}
router={frameworkRouter}
auth={authProvider}
overrides={{
feedback: {
postBottomSlot: (post) => <RequestDiscussion requestId={post.id} />,
},
}}
>
{children}
</StackProvider>Replace route views using feedbackClientPlugin({ pageComponents: { ... } }).
The btst-feedback shadcn registry item ejects the view layer; data hooks remain
owned by the installed package.
Client configuration reference
Prop
Type
Override reference
Prop
Type
Page-scoped presentation
overrides.feedback.pageOverrides on StackProvider supports feedback.PostCard for the feature request board and feedbackRoadmap.PostCard for roadmap columns. Each receives { post: FeedbackPost }.
overrides={{
feedback: {
pageOverrides: {
feedback: { PostCard: ({ post }) => <MyRequestCard post={post} /> },
},
},
}}Only the board uses MyRequestCard; roadmap cards retain their built-in presentation. Missing components fall back directly to the built-in card. Reusable cards outside either page remain unchanged. The post detail and changelog use different content, so they have no PostCard slot. postBottomSlot, permissions, filters, and whole-page replacements retain their existing behavior.
A custom card owns its contents, including any vote button and detail link. The default card keeps its existing permission-checked voting controls; custom voting controls must use the existing voting hooks and permission checks.
Optional plugin bridges
When Feedback and Comments are selected together, the CLI adds a CommentThread
to postBottomSlot with resourceType="feedback-post" and the post slug as
resourceId. It also registers that resource type in comments.resourceLinks
so moderation links return to the Feedback request.
Feedback does not import Comments or Kanban. To enable task linking, pass an
application-owned task lookup to feedbackBackendPlugin:
feedbackBackendPlugin({
kanban: {
getTask: async ({ id, boardId, request }) => {
const operations = request
? myStack.forRequest(request).operations.kanban
: myStack.trusted.kanban
const board = await operations.getBoard({ id: boardId })
return board.columns.flatMap((column) => column.tasks).find((task) => task.id === id) ?? null
},
},
})The linkKanbanTask operation accepts { id, kanbanTaskId, boardId }; id is
the Feedback request ID. Its HTTP route is PUT /feedback/posts/:id/kanban-task.
The operation checks feedback.post.linkKanbanTask and validates the target with
the bridge before storing the optional kanbanTaskId. The lookup must use the
same request's authorized Kanban operations. Pass { id, kanbanTaskId: null } to
unlink. No foreign key is added to the optional Kanban model.
The kanbanTaskSlot(post) override is rendered only when the viewer can link
tasks. The CLI supplies board selection, task creation in the board's first column
using the request content, and existing-task linking when both plugins are selected. A new task is
created through Kanban's own authorized operation before the Feedback link is
saved; these are separate writes. A failed link leaves the task available for
manual linking rather than deleting it. Use useLinkFeedbackKanbanTask from
@btst/plugin-feedback/client/hooks to save a link; it refreshes Feedback posts,
roadmap, and changelog queries. Each returned FeedbackPost includes
kanbanTaskId: string | null.