Build Featurebase

KINDAreplaces $59/mosaves $708/yrback to the verdict

0%0 of 20 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 portal where users post and vote on requests, an admin view that merges duplicates while preserving every voter and comment, themes linking related requests to the evidence behind them, a roadmap generated from status rather than maintained separately, and a changelog that closes the loop with the people who asked.

estimated effort multi-daythe files for this build are in the project pack

RuntimeNode 22 with PostgreSQLIdentityEmail-verified accounts with no password, using signed linksSearchPostgreSQL full text searchEmailA transactional provider with a verified domainDeploymentOne server behind a reverse proxy with automatic certificates

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

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

    Verify node --version prints v22 or higher

  2. installfree

    Why Merging needs transactions and duplicate detection needs full text search. Both are here.

    Get it Install locally or run it in Docker. open ↗

    Verify psql connects and reports 16 or higher

  3. API keyfree tiers usually cover a small portal

    Why Verification links and status notifications are the whole loop. Without delivery, this is a suggestion box nobody hears back from.

    Get it Create an account, add the DNS records they specify, take the credentials. Domain verification is the slow step, so start it first.

    Verify A verification link email arrives in an inbox and is not in spam

  4. have readyroughly 10 to 15 USD a year

    Why Needed for the certificate and for the email sending records. A public portal on a raw address will not be trusted.

    Get it Any registrar, then add the A record and the email verification records.

    Verify The hostname resolves and serves a valid certificate

  5. have readyfree

    Why Duplicate detection, themes and distinct voter counts are all meaningless against invented data, and real feedback contains the near-duplicates the whole design is about.

    Get it Import from wherever feedback currently lives: a spreadsheet, a support inbox, a chat channel. A hundred real items is enough.

    Verify At least a hundred real items are loaded, including some near-duplicates

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- users(id, email, name, verified_at, is_admin, notify_on_status)
- posts(id, title, body, author_id, status, board_id, created_at, merged_into_id, pinned) · merged_into_id is what makes a merge non-destructive
- votes(post_id, user_id, created_at) · unique on the pair, so one person is one vote
- comments(id, post_id, author_id, body, created_at, is_internal)
- themes(id, name, description), theme_posts(theme_id, post_id) · a theme groups requests as evidence for a decision
- changelog(id, title, body, published_at, linked_post_ids_json)
- statuses(key, label, colour, on_roadmap, notify_on_enter) · configurable, because every team's workflow differs
- A merge never deletes. It points one post at another and moves the votes, so the evidence survives and the count is right.

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://user:pass@localhost:5432/feedbackYour PostgreSQL connection string.
PUBLIC_URLrequiredhttps://feedback.example.comAppears in verification links and notifications, so it must be correct before the first submission.
EMAIL_SERVERsecretrequiredsmtp://user:pass@smtp.provider.com:587Transactional provider credentials.
EMAIL_FROMrequiredfeedback@example.comMust be on the verified sending domain.
LINK_SECRETsecretrequiredgenerate-a-long-random-valueSigns verification and unsubscribe links. Rotating it invalidates outstanding links.
MAX_POSTS_PER_EMAIL_PER_DAYrequired5Submission rate limit per address.
ROADMAP_STATUSESrequiredplanned,in-progress,shippedWhich statuses appear on the roadmap. The roadmap is generated from these and maintained nowhere else.

The build, in order

  1. A portal people can actually post to

    Someone can submit a request and vote without creating a password.

    1. Post list sorted by votes, with a submission form asking for as little as possible.

      Files src/board.js

    2. No password. A signed, expiring link that confirms the address and nothing more.

      Files src/auth.js

    3. A unique constraint on post and user. Application-level checks race and eventually double count.

      Files migrations/

    done when · tick each as it passes
    watch out
    • Requiring account creation before someone can post loses most of the feedback you would have received, because people submit in the thirty seconds they are annoyed. Verify by email link after submission rather than requiring a password before it, so the barrier comes after the contribution rather than before.
  2. Catching duplicates before they exist

    Someone about to post the fourth copy of a request sees the first three.

    1. A generated search column with an index. Indexing at write time rather than searching with wildcards.

      Files migrations/

    2. Debounced, showing vote counts, because a request with two hundred votes is more persuasive than one with two.

      Files src/suggest.js

    3. Without leaving the form. Any friction here and people post the duplicate anyway.

      Files src/suggest.js

    done when · tick each as it passes
    watch out
    • Merging duplicates after the fact is real work that never finishes, and the vote counts are misleading until it is done. Search at submission time is the cheapest possible intervention and prevents most duplicates, provided voting on the suggestion is one click rather than a navigation away.
  3. Merging without losing anything

    Two requests become one, and nobody's vote or comment disappears.

    1. Votes moved with deduplication, comments moved with provenance, the source pointed at the target. All or nothing.

      Files src/merge.js

    2. Someone who voted on both is one vote in the target. Test with a hand-counted case.

      Files src/merge.test.js

    3. People have shared it. A dead link makes the merge look like deletion.

      Files src/routes.js

    done when · tick each as it passes
    watch out
    • Deleting the duplicate loses its comments and breaks every link anyone shared, including from support tickets and chat. Keep the post and point it at the target, so an old link redirects and the discussion history stays intact.
  4. Themes, which is where decisions come from

    Related requests group into a theme you can act on, with the evidence attached.

    1. A problem statement plus its evidence. Themes are how you decide; posts are how you gather.

      Files src/themes.js

    2. Across the theme's posts, deduplicated. Test with an overlap you constructed.

      Files src/themes.test.js

    3. Every post in the theme, so a decision can be justified from what people actually said.

      Files src/themes.js

    done when · tick each as it passes
    watch out
    • Summing vote counts across a theme's posts double counts anyone who voted on more than one, which inflates the theme and misranks it against others. Count distinct voters, which is the number that actually answers how many people want this.
  5. Roadmap and changelog that close the loop

    Status changes are visible, and the people who asked are told when it ships.

    1. Configurable which statuses appear. No separately maintained list anywhere.

      Files src/roadmap.js

    2. The link is what makes the notification meaningful and the loop closed.

      Files src/changelog.js

    3. An outbox with a sent record per user per entry. Publishing twice must not notify twice.

      Files src/notify.js

    done when · tick each as it passes
    watch out
    • A roadmap maintained as its own list drifts from the actual statuses within weeks, and then it lies to people. Generate it from status so it cannot disagree, and let configuring which statuses appear be the only control you need.
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.