Getting started
Install once from the repository root, bring up every dev server with one command, and know which gate to run before you push.
This walks a fresh clone to a running workspace. It takes about five minutes,
most of which is bun install.
What you need
| Tool | Version | Needed for |
|---|---|---|
| Bun | 1.3.9+ | The package manager and test runner for the whole workspace |
| Node | 20+ | Next.js tooling still shells out to it |
| Go | 1.22+ | Only apps/statshub-api |
| Docker | any | Only the local Postgres and the parity stack |
| Xcode or Android Studio | current | Only apps/statshub-expo |
Install
Once, from the repository root. Bun hoists the workspace, so installing inside
an app is never the right move — it rewrites bun.lock and the next
turbo prune reads that lockfile.
bun installCopy the environment files
Each app carries its own .env.example. Copy the ones for the apps you plan to
run. Without them the servers still start and then fail at request time, which
looks like a broken build and is not.
cp apps/statshub-web/.env.example apps/statshub-web/.env.local # Supabase URL + anon key
cp apps/statshub-admin/.env.example apps/statshub-admin/.env.local
cp apps/statshub-api/.env.example apps/statshub-api/.env # DATABASE_URL
cp apps/statshub-expo/.env.example apps/statshub-expo/.env.localTwo failures are worth recognising on sight:
webwithout the Supabase keys answers 500 withYour project's URL and API key are required.apiwithoutDATABASE_URLlogsDATABASE_URL is requiredand exits — though/api/healthanswers first, so a health check alone will not catch it.
Start the local Postgres
Only if you are touching data. web, admin and api all read the same
database.
bun run db:dev:up # docker compose up -d postgres
bun run db:dev:psql # a shell on it
bun run db:dev:reset # drop the volume and start cleanRun everything
bun run devThat starts every dev server except legacy:
| App | URL |
|---|---|
docs | http://localhost:3001/docs |
web | http://localhost:3002 |
admin | http://localhost:3003 |
playground | http://localhost:3004 |
api | http://localhost:8080/api/health |
expo | Metro on 8142 — open the Pitsi Dev Client |
Run one on its own with bun run dev:web, dev:admin, dev:docs,
dev:playground, dev:api or dev:expo. bun run dev:legacy starts the old app on 3000; it is being
replaced by web and is excluded from dev deliberately.
Each dev script clears its own port first. A previous run that outlived its
terminal still holds the port, and the only symptom is bind: address already in use, so scripts/free-port.sh stops the old process before the new one
starts. It only ever stops processes belonging to this repository — if
something else owns the port it says what, and that server does not start.
bun run kill stops all of them at once, which is the one to reach for when a
run is wedged rather than merely stale.
One server failing will not stop the others
dev passes --continue, so a missing binary or a port held by another
project takes down only that server. Read the log rather than assuming the
whole run died.
Reaching a dev server from another device
The dev box is a remote server, so localhost only means anything in a shell on
it. Every other device — your laptop, your phone — arrives over the tailnet, and
the dev servers are configured for that by default. Nothing to pass, no flag to
remember.
bash scripts/tailnet-host.sh # this machine's tailnet addressSwap localhost for that address, or for the machine's MagicDNS name, and every
URL in the table above works unchanged.
Two things had to be true for that to hold, and both are handled centrally in
packages/statshub-tooling rather than per app:
- Next refuses cross-origin
/_next/*requests in dev. Left alone it serves the HTML and then 403s every chunk, so the page arrives with no JavaScript and no styling — which reads as a broken app, not a missing setting.withStatshubDefaultsfills inallowedDevOriginsfrom the running Tailscale daemon, including the MagicDNS wildcard, so a device that joins the tailnet later needs no config change. - Metro advertises whatever host it was started with. The Expo dev script
passes the tailnet address as
REACT_NATIVE_PACKAGER_HOSTNAME, so the dev client on a phone is handed an address it can actually route to.
Nothing is pinned to a particular tailnet: the address and the MagicDNS suffix
are read at startup. On a machine with no Tailscale the lookup returns nothing
and every app falls back to its ordinary localhost behaviour, so this costs a
laptop-only checkout nothing. Set TAILNET_DEV_ORIGINS to a comma-separated
list to override — a reverse proxy or a shared origin Tailscale cannot know
about.
Dev ports are not public
The box exposes 22, 80 and 443 to the internet and nothing else. Dev ports are
reachable only over the tailscale0 interface, which is a firewall rule rather
than an accident — keep it that way.
Before you push
bun run check # lint + typecheck + test across every workspaceThe individual gates are bun run lint, bun run typecheck, bun run test and
bun run build. Two apps have gates of their own that check does not cover:
bun run build:docsresolves every icon name in this site, andbun run lintinsideapps/statshub-docsresolves every internal/docslink.devserves broken ones happily.bunx expo export --platform iosinapps/statshub-expocatches native resolution failures thattsccannot see. See the mobile docs.