StatsHub Docs

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

ToolVersionNeeded for
Bun1.3.9+The package manager and test runner for the whole workspace
Node20+Next.js tooling still shells out to it
Go1.22+Only apps/statshub-api
DockeranyOnly the local Postgres and the parity stack
Xcode or Android StudiocurrentOnly 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 install

Copy 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.local

Two failures are worth recognising on sight:

  • web without the Supabase keys answers 500 with Your project's URL and API key are required.
  • api without DATABASE_URL logs DATABASE_URL is required and exits — though /api/health answers 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 clean

Run everything

bun run dev

That starts every dev server except legacy:

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 address

Swap 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. withStatshubDefaults fills in allowedDevOrigins from 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 workspace

The 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:docs resolves every icon name in this site, and bun run lint inside apps/statshub-docs resolves every internal /docs link. dev serves broken ones happily.
  • bunx expo export --platform ios in apps/statshub-expo catches native resolution failures that tsc cannot see. See the mobile docs.

Where to go next

On this page