Skip to content

Running the app locally

For changing BetterVoting itself — a UI fix, a results page, an API change. Nothing here is needed to edit documentation; see docs_site.md for that.

The full write-up, including the two setups and seven hard-won gotchas, lives in the library repo: running_bettervoting_locally.md. This page is the short start/stop card; go there when something breaks.

Use the script

The commands below are what ./bv runs; prefer the script, which also gets the three things this page used to get wrong (see Quick diagnosis).

./bv status

status says what's up and whether the clone you're in can even run. Then bv prep <clone> for a cold one, bv up to start, bv down to stop. bv up with no argument uses the clone you're standing in.

Which setup

I want to… Use
Click through the UI — vote, create an election, check a results page dev flow (localhost:3000)
Run the Playwright E2E suite docker compose
Unit tests, tsc, lint neither — just npm

The Docker stack is an E2E harness, not for manual clicking: it serves at http://web:5000, which isn't a secure context, so crypto.randomUUID is missing and the Create Election wizard silently does nothing.

Start — dev flow

From /Volumes/T7/Voting/BetterVoting/BV/bettervoting:

docker compose down                                    # if the E2E stack is up, frees 5432/8080
brew services start postgresql@16
docker compose up -d --build keycloak
npm run build -w @equal-vote/star-vote-shared          # only if you edited packages/shared/
npm run dev -w @equal-vote/star-vote-backend           # terminal 1 — wait for "Server started on port 5001"
npm run dev -w @equal-vote/star-vote-frontend          # terminal 2 — opens localhost:3000

Frontend localhost:3000 · backend localhost:5001 · keycloak localhost:8080 · postgres localhost:5432.

Login: admin/admin is the Keycloak master console, not an app user — it will always fail. Register your own Dev-realm user at localhost:8080.

Stop — dev flow

# 1. the two dev servers: Ctrl+C in each terminal, or if one is orphaned:
pkill -f "tsx watch"

# 2. keycloak (and anything else in the compose stack):
docker compose down

# 3. postgres — usually worth leaving up:
brew services stop postgresql@16

Start / stop — E2E stack

docker compose up --build     # runs the Playwright suite automatically
docker compose down           # stop and remove

Quick diagnosis

Symptom Cause
EADDRINUSE :5001 stray backend — kill it by port, kill $(lsof -ti tcp:5001). Not pkill -f "tsx watch": that matches the two supervisor processes but not the child actually bound to the port (whose command line reads node --require …/tsx/dist/preflight.cjs), so it looks like it worked and the port stays held.
port 5000 in use macOS AirPlay Receiver (turn it off in System Settings) — or a BetterVoting backend, since 5000 is the stock port. Check before blaming AirPlay: lsof -nP -iTCP:5000 -sTCP:LISTEN.
UI loads but every API call fails, no error anywhere A mismatched .env pair — see below.
Bind for 0.0.0.0:8080 failed: port is already allocated You ran docker compose up keycloak from a second clone. Compose names the project after the clone directory, so it tried to build an independent keycloak fighting for the same host port. One keycloak serves every clone — always start it from BV/bettervoting, and clear the orphan with docker rm <clone>-keycloak-1 && docker network rm <clone>_star-net.
Module not found: @equal-vote/star-vote-shared/... rebuild shared: npm run build -w @equal-vote/star-vote-shared
Could not read package.json you're in your home dir — cd to the repo root
node: command not found fish_add_path /usr/local/opt/node@20/bin
__META_TITLE__ in the browser tab normal under the dev flow — meta injection only runs in the backend-served build

The backend port is per-clone

There is no single right backend port, and assuming one will cost you an afternoon.

  • Upstream's sample.env ships a 5000 pair — BACKEND_PORT=5000 in the backend, REACT_APP_BACKEND_URL=http://localhost:5000 in the frontend.
  • BV/bettervoting has been hand-edited to a 5001 pair.

Both work. What breaks is a mismatched pair, and it breaks silently: the backend starts fine on whichever port it read, the UI loads, and every API call goes to a dead port with no error anywhere. Two ways to get there — .env is gitignored, so a fresh clone has none and packages/backend/src/index.ts:10 falls back to BACKEND_PORT || 5000; or you copy one half of a good .env in and leave the other.

bv status prints each clone's backend port and flags a mismatch; bv prep copies both halves and refuses to leave a clone mismatched; bv up reads the port from the clone rather than assuming.

Sandbox — no local stack needed

For checking how a tabulation renders without running anything, https://bettervoting.com/sandbox tabulates with live code and mints nothing. Caveat: Race Details is permanently Loading... there, so table-level checks still need a real election.