Build Nolt
YESreplaces $29/mosaves $348/yrback to the verdict
A feedback board where the engineering problem is vote integrity on a public page without demanding accounts, and the design problem is that a roadmap built from raw vote counts is a popularity contest that will mislead you about what your users actually need.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the server and the board.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree locally
Why Vote uniqueness is enforced by a constraint, and merging correctly needs set operations.
Get it Docker or your package manager. open ↗
Verify
A unique constraint rejects a duplicate vote insert - decidefree
Why Users read a roadmap column as a promise, and an undefined status is read optimistically. Deciding the meanings after publishing means changing what you appeared to promise.
Get it Write one sentence per status, including explicitly what planned does not mean. Decide whether you will ever show dates.
Verify
A written meaning exists for every status and appears on the board - decidefree
Why Ranking by raw count hands your roadmap to whoever is loudest, and segmentation is only possible if you decide early what you can actually know about a voter.
Get it Decide what distinction matters to you and what evidence you will have for it without demanding accounts.
Verify
A written note names the segments and how a voter is assigned to one - have readyfree
Why Duplicate detection and merging only become interesting at volume, and a board with ten posts has no duplicates to find.
Get it Export from wherever feedback currently lands, including the ten near-identical versions of the same request.
Verify
A hundred items exist including several genuine duplicates
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- boards(id, slug, title, visibility, allow_anonymous, created_at) - posts(id, board_id, title, body, author_id, status, created_at, merged_into_id) - votes(post_id, voter_id, weight, at) · one row per voter per post, enforced by a constraint - voters(id, kind, account_id, fingerprint_hash, first_seen_at, segment) · kind is account or anonymous - merges(id, kept_post_id, merged_post_id, votes_combined, at) - comments(id, post_id, voter_id, body, at) - status_changes(post_id, from_status, to_status, note, at) · append only, because a status that quietly reverts is worse than one that never moved
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://localhost:5432/feedback | Posts, votes and voter identities. |
ALLOW_ANONYMOUS_POSTS | required | true | Requiring accounts collapses submission volume. This is the trade, made explicit. |
VOTE_RATE_LIMIT_PER_HOUR | required | 20 | Per identity and per network. Generous for a person, tedious for a script. |
BURST_QUARANTINE_THRESHOLD | required | 10 | Votes from one source in a short window before quarantine. Quarantine, not deletion. |
STALE_STATUS_DAYS | required | 120 | How long an item may hold a status before being surfaced to you as stale. |
SMTP_URLsecret | optional | smtp://user:pass@host:587 | Status change notifications. Absent means the board works without them. |
The build, in order
A board people will actually post on
Someone can submit a request in under a minute without creating an account.
The friction is what you are trading feedback volume against.
Files
src/posts.tsPersisting across restarts, so voting can be one per person.
Files
src/voters.tsSomeone signing up later should not lose their votes.
Files
src/claim.ts
done when · tick each as it passeswatch out- Requiring an account before someone can post removes most of the feedback you would have received, and the people who do sign up are not a representative sample of your users. Let them post immediately, and offer the account for the thing accounts are actually good for, which is being told what happened.
Votes that are hard to fake
Voting is easy for a person and tedious for a script.
A unique constraint, not an application check.
Files
src/schema.sqlBoth, since either alone is trivially avoided.
Files
src/ratelimit.tsA burst may be an attack or a customer's whole team. Keep the distinction.
Files
src/integrity.ts
done when · tick each as it passeswatch out- Silently discarding suspected fraudulent votes means you cannot tell the difference between an attack and a legitimate burst from a customer who told their team to go and vote, and the second is exactly the signal you wanted. Flag and quarantine for review rather than deleting, so the distinction stays available.
The same request, ten times over
Duplicates are found and merged without losing anyone's vote.
Adding the totals double-counts anyone who voted for both.
Files
src/merge.tssrc/merge.test.tsThe cheapest moment to prevent one is before it is written.
Files
src/duplicates.tsTheir author should find out where their request went.
Files
src/posts.ts
done when · tick each as it passeswatch out- Merging by adding the vote counts double-counts everyone who voted for both posts, which inflates the merged total and moves a request up the board on votes that do not exist. Combine the voter sets and count the union.
Statuses that do not promise things
A roadmap communicates intent without being read as a commitment.
On the board itself. Users infer meanings otherwise, usually optimistic ones.
Files
src/statuses.tsA silent reversal from planned to open is worse than never planning it.
Files
src/status-changes.tsEight months in planned is information, and mostly for you.
Files
src/staleness.ts
done when · tick each as it passeswatch out- A planned column with no dates and no movement for eight months is read by users as a broken promise, and by the operator as an accurate representation of intent, and both are behaving reasonably. Show how long an item has held its status, and surface the stale ones so the board reflects what is actually happening.
Reading the board honestlyproduct builder
You can see who wants what, not just how loud the total is.
Beside the total, not instead of it. The breakdown is the useful part.
Files
src/segments.tsIt is the main reason anyone creates an account here.
Files
src/notify.tsPosts, votes, merges and history. It is your users' input, not yours.
Files
src/export.ts
done when · tick each as it passeswatch out- Ranking by raw vote count means the loudest audience decides the roadmap, which is frequently not the paying one and almost never the silent majority. Show the breakdown by segment beside the total, so a request with forty votes from non-customers and one from your largest account is legible as what it is.
That is the whole plan for Nolt. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Single sign-on with your product.
- Moderation at scale.
- Multi-language boards.
- Deep issue tracker integrations.
- hosted simplicity, custom domains, integrations, and low maintenance
- large participant panel
- recording and transcription infrastructure
- advanced research repository
- enterprise governance
- Private boards for a specific customer, which changes the segmentation question
- Linking a post to what actually shipped, which is what closes the loop
- A digest of what moved this month, which is the notification people want most
Need the files? The project pack on the verdict page hands your agent the whole brief · more user research.