Build WishKit

KINDAreplaces $15/mosaves $180/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 feature request board whose hard problems are an API that must keep working for binaries you shipped last year, a device identifier that is a hint rather than an identity, and abuse handling on endpoints anybody on the internet can post to.

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

RuntimeNext.js 15 with the App Router and TypeScriptStorageSQLite through Drizzle, one fileIdentityA client-generated identifier treated as a hint, never as proofAPIA versioned path with fields that are only ever addedAdminOne password from the environment behind a signed cookie

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 board, the admin view and the API.

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

    Verify node --version prints v22 or higher

  2. have readya few pounds a month

    Why The endpoint has to answer for as long as an old build of your app is installed somewhere, which is longer than you think and not something you can shorten.

    Get it Any small virtual machine or container platform with a volume and a domain you control.

    Verify The health endpoint answers over TLS at a name you own

  3. have readyfree with most hosts

    Why Application rate limiting protects the database and does nothing about the bandwidth, so the layer in front needs a limit too before a public write endpoint meets its first script.

    Get it Any reverse proxy or platform feature that limits requests per source.

    Verify A burst from one source is refused before it reaches the application

  4. installfree, on a Mac

    Why The small SwiftUI package is the part that makes this worth self-hosting, and it cannot be compiled or tested anywhere else.

    Get it Xcode from the Mac App Store.

    Verify The package builds and the board view lists wishes from your API

  5. decidefree

    Why It is a promise to people running software you can no longer change, and picking the number after the first breaking change means picking it too late.

    Get it Decide a minimum period an API version is served after it is superseded, and write it in the README.

    Verify The figure is in the README and the old version still answers

Data model

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

- projects(id, name, api_key, created_at) · the key identifies an app, not a person, and it will end up public
- wishes(id, project_id, title, description, status, created_at, author_device_id, hidden_at, hidden_reason)
- votes(wish_id, device_id, created_at) · unique on the pair, which is the only deduplication available to you
- comments(id, wish_id, device_id, body, created_at, hidden_at) · hidden rather than deleted, so a moderation decision is reviewable
- merges(from_wish_id, into_wish_id, merged_at, votes_moved, votes_dropped) · a merge is a recorded event, not an update
- abuse(bucket, window_start, count, blocked_until) · per device and per source, because a public write endpoint attracts the internet
- The vote count is the sum of rows in votes, and every row came from a client that told you who it was. The board should present it as a signal, not a measurement.

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_PATHrequired./data/wishes.dbOne file. Backed up on a schedule with a tested restore.
ADMIN_PASSWORDsecretrequireda long random stringThe only credential in the system. Rotate it by changing this and restarting.
COOKIE_SECRETsecretrequiredredacted-32-byte-randomSigns the admin session cookie. Changing it signs everyone out, which is a feature.
PUBLIC_BASE_URLrequiredhttps://wishes.example.comWhat your shipped apps call. Changing it strands old builds, so choose once.
WRITE_LIMIT_PER_HOURrequired10Per device identifier. Stated in the error so a blocked caller knows why.
MAX_BODY_CHARSrequired2000Titles, descriptions and comments. Enforced before storage, not after.
API_DEPRECATION_DAYSrequired365How long a superseded API version keeps answering. Published in the README.

The build, in order

  1. A board where new things are not visible yet

    Wishes, votes and comments work and nothing a stranger submits appears without a decision.

    1. Moderation by default, without a form that looks broken.

      Files src/board/submit.ts

    2. Two wishes with five votes should not swap places on every refresh.

      Files src/board/sort.ts

    3. A wish that vanishes gets submitted again by the same person next week.

      Files src/board/statuses.ts

    done when · tick each as it passes
    watch out
    • Publishing submissions immediately means the first time somebody posts abuse to your public board it is live on a page linked from inside your app, visible to every customer, until you happen to look. Default new wishes to pending, show the submitter their own pending wish so the form does not feel broken, and make approval a deliberate act.
  2. An API you cannot take back

    The endpoints are versioned and shaped so a build shipped today keeps working next year.

    1. The cheapest thing you will ever do for the version of yourself who ships a change.

      Files src/app/api/v1/route.ts

    2. The fixture is the shipped client you cannot run.

      Files test/fixtures/v1test/compat.test.ts

    3. It is inside a binary anybody can download and unpack.

      Files docs/api.md

    done when · tick each as it passes
    watch out
    • Renaming a response field or tightening a validation rule breaks every copy of your app that people already installed, and unlike a website you cannot fix them, you can only wait for updates that some users will never install. Version the path, add fields rather than changing them, and treat a shipped client as a contract you signed.
  3. A device identifier is a hint

    Duplicate votes are discouraged honestly and the board never claims the count is exact.

    1. A flaky mobile network retries, and a retry must not be a second vote.

      Files src/api/vote.tstest/idempotent-vote.test.ts

    2. Either one alone is trivially defeated by the other.

      Files src/abuse/limits.ts

    3. Honesty here costs one sentence and prevents one bad roadmap.

      Files src/board/VoteCount.tsx

    done when · tick each as it passes
    watch out
    • Treating a device identifier as a person produces a vote count people make roadmap decisions with, and it is wrong in both directions. Apple documents that the vendor identifier changes when a user deletes all of that vendor's apps and reinstalls, and that it can be nil after a restart before the device is unlocked, so an honest user can vote twice; meanwhile anyone can send whatever identifier they like to a public endpoint. Deduplicate on it, rate limit around it, and describe the number as interest rather than as a count of people.
  4. Merging duplicates without losing votes

    Two wishes for one idea become one, and the arithmetic is recorded.

    1. The unique pair is already the right answer; adding totals is not.

      Files src/admin/merge.tstest/merge.test.ts

    2. Somebody will ask why the number went down.

      Files src/admin/merge.ts

    3. Links live in chat logs and in shipped app builds.

      Files src/board/redirect.ts

    done when · tick each as it passes
    watch out
    • Adding the two vote totals together after a merge double counts everybody who voted for both wishes, and since duplicates are exactly the wishes the same people vote for, the merged total is inflated precisely where it matters most. Move rows and let the unique constraint drop the overlap, then show both numbers.
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.