Build Nolt

YESreplaces $29/mosaves $348/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

RuntimeTypeScript with Fastify and PostgreSQLIdentityOptional accounts, with a durable per-browser identity for anonymous votingIntegrityRate limits, duplicate detection and vote weighting by segmentMergingDuplicate posts merged with votes combined and voters deduplicatedStatusExplicit statuses with a stated meaning, and no implied commitment

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the server and the board.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

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

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

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

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

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://localhost:5432/feedbackPosts, votes and voter identities.
ALLOW_ANONYMOUS_POSTSrequiredtrueRequiring accounts collapses submission volume. This is the trade, made explicit.
VOTE_RATE_LIMIT_PER_HOURrequired20Per identity and per network. Generous for a person, tedious for a script.
BURST_QUARANTINE_THRESHOLDrequired10Votes from one source in a short window before quarantine. Quarantine, not deletion.
STALE_STATUS_DAYSrequired120How long an item may hold a status before being surfaced to you as stale.
SMTP_URLsecretoptionalsmtp://user:pass@host:587Status change notifications. Absent means the board works without them.

The build, in order

  1. A board people will actually post on

    Someone can submit a request in under a minute without creating an account.

    1. The friction is what you are trading feedback volume against.

      Files src/posts.ts

    2. Persisting across restarts, so voting can be one per person.

      Files src/voters.ts

    3. Someone signing up later should not lose their votes.

      Files src/claim.ts

    done when · tick each as it passes
    watch 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.
  2. Votes that are hard to fake

    Voting is easy for a person and tedious for a script.

    1. A unique constraint, not an application check.

      Files src/schema.sql

    2. Both, since either alone is trivially avoided.

      Files src/ratelimit.ts

    3. A burst may be an attack or a customer's whole team. Keep the distinction.

      Files src/integrity.ts

    done when · tick each as it passes
    watch 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.
  3. The same request, ten times over

    Duplicates are found and merged without losing anyone's vote.

    1. Adding the totals double-counts anyone who voted for both.

      Files src/merge.tssrc/merge.test.ts

    2. The cheapest moment to prevent one is before it is written.

      Files src/duplicates.ts

    3. Their author should find out where their request went.

      Files src/posts.ts

    done when · tick each as it passes
    watch 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.
  4. Statuses that do not promise things

    A roadmap communicates intent without being read as a commitment.

    1. On the board itself. Users infer meanings otherwise, usually optimistic ones.

      Files src/statuses.ts

    2. A silent reversal from planned to open is worse than never planning it.

      Files src/status-changes.ts

    3. Eight months in planned is information, and mostly for you.

      Files src/staleness.ts

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more user research.