BTST

Feedback Plugin

Feature requests, voting, a public roadmap, and linked release notes

Full-stackReleased · Preview

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.

Real BTST Feedback roadmap with planned, in-progress, and shipped feature requests.
Feedback stores requests and votes and presents them on a public roadmap.

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

  1. 1Collect requests

    Users submit ideas and vote once per request.

  2. 2Plan delivery

    Administrators move requests through Planned, In Progress, and Shipped.

  3. 3Publish releases

    Link shipped requests to changelog entries.

Installation

pnpm add @btst/plugin-feedback@1.1.0

Register 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).

RouteBehavior
/feedbackFeature requests, category/status filters, submission, and votes
/feedback/[slug]Request details, voting, and administrator status changes
/feedback/roadmapPlanned, In Progress, and Shipped columns
/feedback/changelogRelease 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.

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