Build a Canny alternative
Build a Canny alternative as a feedback board with votes and a status column. $79–$99 a month is rent you can cancel after a 10-hour weekend v1.
You can build a Canny alternative as a board people search before they post. Canny is that product. The catalog line is “Collect requests, count votes, close the loop.”
You pay $79–$99 a month for it. That is $948–$1,188 a year. The basic replacement is 10 hours: a feedback board, votes, and statuses.
I would ship this if I still rented a vote board for customers I already have. It is marked Weekend. The verdict on the card is “Painfully replaceable”.
Yesterday I wrote about vibe coding your own Shopify. That was a teardown. This one is a regular backlog topic. The useful v1 is the board plus a status people can see.
What you need to build a Canny alternative
The Canny page splits the work into three rows.
The first row is the one you want this weekend. A public or private board, search before posting, one vote per person, and four statuses: planned, in progress, complete, declined. Title, detail, category, author, a count.
The second row is 1 week. Comments, a roadmap view, a changelog, single sign-on, email notifications. Those can wait until one real request already moved from open to planned.
The third row is a month and up. AI deduplication, CRM sync, a multi-workspace SaaS. The table labels that row as the part you should skip.
If you tell an agent to “clone Canny”, it aims at the third row. Copy the prompt, then pin it to the first one.
Here is the shape of the data. Nothing else has to exist at 10 hours.
const request = {
id: 'req-dark-mode',
title: 'Dark mode for the booking page',
body: 'The Slot booking page blinds people after 9pm.',
category: 'ui',
author: 'U-ada',
status: 'open',
votes: 1,
}
Ada searches “dark mode”, finds nothing, posts that row, and becomes the first vote. You later set status to planned. She can see the request is on the list without emailing you for an update.
How does Canny work
I am describing the catalog’s model here, not reverse-engineering Canny. A feedback board is a list of requests other people can find. A new post should lose to an existing one with votes on it.
The screens on the page are a board, a request detail, a team triage queue, and a roadmap plus changelog. For 10 hours you can live with the board and the detail page. Sort by top, new, or recent activity. Filter by category and status.
The data model is User, Request, Vote, Comment, ChangelogEntry. This weekend you need User, Request, and Vote. Comment and ChangelogEntry belong on the week-long row.
The capability line is Astro, HTMX, and D1. This site is an Astro app on Cloudflare, so that is the path I default to. Use Rails if that is your weekday language. Scope matters more than the framework.
Identity comes from the app’s sign-on payload, verified on the server. Do not trust a name the browser sent. One human maps to one local user. A private board shows nothing to an invalid payload: no titles, no counts, no 404 that confirms the slug.
Who a Canny alternative is for
The audience line on the page is specific. A product team collecting feedback from existing customers.
The goal is a single place for requests, plus a visible next step. This weekend, one board your users can search and one status you can change in public.
If you need NPS surveys, CRM enrichment, or a second workspace for another product, keep paying. The site puts that rebuild at a month on purpose.
The same v1 also replaces Featurebase, Nolt, and Upvoty. One focused build covers whichever of those you currently pay for.
A form that collects a lead is Typeform, $29–$99 a month, about 2 hours. The Typeform walkthrough is that post. Build this if people already have an account and they want to vote.
One vote per person
The first mini-prompt on the page is “One person, one vote, no matter how hard they click”.
A unique constraint on (user_id, request_id) is the guarantee. The application check is a courtesy. A double-click, a retry, or a replayed request hits the constraint, changes nothing, and returns the current state.
Keep the displayed total as a counter on the request, updated in the same transaction as the vote row. A counter you bump in a second write will drift. A cheap job that compares the counter to the real row count is worth writing once you have traffic.
async function vote(db, userId, requestId) {
try {
await db.transaction(async (tx) => {
await tx.insertVote({ userId, requestId })
await tx.incrementVotes(requestId)
})
} catch (error) {
if (error.code === 'UNIQUE') {
return db.getRequest(requestId)
}
throw error
}
return db.getRequest(requestId)
}
Unvoting deletes the row and decrements in the same transaction. Rapid vote-unvote-vote has to serialize to the last click, not to a leftover count from a race.
Identity stays on the server. A name or id from the browser is not a voter.
Search before you post
The second mini-prompt is “Search before you post, merge when they post anyway”.
As someone types a title, search existing requests and show matches with their vote counts. “12 people already want this” turns a duplicate into a vote. Stemmed keyword overlap catches a lot of “dark mode” versus “night theme” before you reach for anything fancier.
Duplicates still get posted. Merge is the triage tool for that. Fold the extra request into the canonical one. Votes combine as the union of distinct voters, never the sum. Someone who voted on both counts once. Comments move with a note that they came from a merge. The old URL redirects, so a shared link still works.
Weekend v1 can stop at live search. Merge is the week-long version of the same problem. If you skip search, you will spend Saturday merging by hand.
Statuses that close the loop
Statuses on the page are planned, in progress, complete, and declined. Record every transition with actor and time. The public detail page shows the current one. The history is for you.
Notifications belong on the week-long row. When you add them, enqueue only after the transition commits. Notifying inside the transaction is the classic bug: a rollback after enqueue tells 200 people about a status that never happened.
Deduplicate with a key per user, request, and transition. A retried worker, or an admin flipping complete to open and back, should not multiply emails. A person who is voter, commenter, and subscriber gets one message.
The complete transition should point at a changelog entry so the mail says what shipped. That entry is a title, a date, and a summary. Beamer is the product if a changelog is the only rent you pay: $49–$299 a month, about 4 hours for a public feed.
Team notes the customer cannot see
The last mini-prompt is “Team notes the customers can never see”.
Model public comments and team-only notes as two types, not a flag on one table. A boolean is one missing WHERE away from leaking “this customer is churning” onto the public page.
Filter by viewer role at the query. Render through serializers that name the fields each audience may see. Run exports through the same serializers. Compose notification mail from customer-visible content only.
Write the leak test first. Create a note, then assert it is missing from the public API, the rendered page, the CSV, and the outbound email.
A private board uses the same rule at the door. An unauthenticated request discloses nothing.
Canny alternative or an issue tracker
A board of customer votes is not an issue tracker. Linear is $10–$16 a person a month. Title, status, assignee, a backlog. The Linear teardown is that post. You can vibe-code the tracker. The sync engine is the other product.
I would not put customer requests in Linear and call that a Canny alternative. Customers cannot search it. They cannot see a public status. You end up screenshotting issues into a changelog by hand.
Keep Linear for the work you accepted. Keep the vote board for the work you have not accepted yet. When a request becomes planned, file the Linear issue yourself. Do not sync the two in v1.
A one-question intake form is still Typeform. A Canny alternative assumes the person already has an identity you trust.
What not to build in a Canny clone
Leave out NPS surveys, CRM enrichment, AI deduplication, and multiple workspaces.
The non-goal on the page already names those. You are replacing the board you currently rent for one product.
If the agent starts a second workspace or a HubSpot import, stop it and paste the non-goal again.
The week-long row, after one real request
Use the board for a week as the real inbox, not as a demo. Then look at the second estimate.
Add comments under a request, a roadmap with planned, in progress, and complete columns, and changelog entries linked to completed requests. App single sign-on means customers skip a second password. Email when a status moves, and only after commit.
The catalog says 1 week for that row. Stop there. Skip the month-long platform.
Host it, take real votes, cancel
The product pages put hosting this way: a focused replacement costs your build time plus close-to-zero hosting.
This one wants Astro, HTMX, and D1, because that is the stack on the card. There is a table for requests and votes, so take a backup before you delete the paid account.
Put the board on a subdomain you already own, board.flaviocopes.com. Paste the current Canny export, or type the ten requests people already repeat in email. Search for one of them. Vote as a second user. Change the status. Confirm the first user can see it.
If you miss comments or a changelog, add them from the week-long row.
Then cancel Canny, or Featurebase, or Nolt, or Upvoty, whichever one you pay for.
Start now
Go to Build your own Canny. Copy the AHA prompt, or the stack you already write.
Tell the agent the first estimate only: feedback board, votes, statuses. Ten hours.
Search before the first post. Move one real request to planned this weekend.