You just launched your SaaS. Users are signing up, but you are not sure what to build next. Someone tells you to "just use Canny." Then you look at the pricing: $79 per month for a feature request board.
That is a real cost before you have real revenue. And it means your user feedback lives in someone else's database, visible only through their dashboard.
There is a better path. A feature voting board is a few database tables and a handful of pages. If you are already using the Next.js SaaS boilerplate, you can build it this weekend and keep every user suggestion inside your own app.
Strip away the pricing tiers and it comes down to four things:
Nothing in that list requires a third-party tool. It requires a schema, two API routes, and a page.
With Drizzle ORM and Neon DB, the schema looks like this:
export const featureRequestTable = pgTable('feature_requests', {
id: uuid('id').defaultRandom().primaryKey(),
authorId: uuid('author_id').notNull().references(() => userTable.id),
title: text('title').notNull(),
description: text('description'),
status: text('status').notNull().default('open'), // open | planned | in_progress | shipped
voteCount: integer('vote_count').notNull().default(0),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const featureVoteTable = pgTable('feature_votes', {
id: uuid('id').defaultRandom().primaryKey(),
userId: uuid('user_id').notNull().references(() => userTable.id),
requestId: uuid('request_id').notNull().references(() => featureRequestTable.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
}, (t) => [unique().on(t.userId, t.requestId)]);
The unique constraint on (userId, requestId) means one vote per user per feature -- no gaming the board.
When a user clicks upvote, you insert a row into feature_votes and increment voteCount on the request. When they click again, you delete the vote and decrement. Both writes are wrapped in a Drizzle ORM transaction so the counter never drifts.
Your service layer owns this logic. The route calls featureService.toggleVote(requestId, userId) -- no business logic leaks into the handler.
Since auth is already wired via JWT, you get the userId from getUserFromRequest(req) at the top of every route. No extra setup. You already know who is voting.
The board page is a server component. It fetches all open requests sorted by voteCount descending and shows which ideas the current user has already voted for. No client-side fetch needed for the initial render -- the list is ready by the time the page lands.
A filter lets users switch between status tabs: All, Planned, In Progress, Shipped. This is the same pattern as any data table in the boilerplate: URL search params drive a server component query, and Shadcn UI renders the tabs.
Once you collect enough votes, you can pair the board with feature flags to ship the top-voted features to your most engaged users first, before rolling out to everyone.
Build it yourself if:
Use Canny if:
For most early-stage SaaS products, the self-built board wins. You already have the auth, the database, the component library, and the service layer pattern. The incremental cost is an afternoon.
Your users submit feedback inside the product they are already logged into. You ship the update, change the status to Shipped, and the board reflects it immediately.
The Next.js SaaS boilerplate already has JWT auth, Drizzle ORM, Neon DB, and Shadcn UI wired up. Add the two tables from the schema above, write a service with toggle-vote logic, and you have a working feedback board before your next standup.
Your users have opinions about what to build next. This is the cheapest way to hear them -- and the only way to keep that signal where it belongs: inside your own product.