How apps find each other
One app's address is never written down in another. `appUrl("playground", "/birds-eye-view")` reads the port from that app's own dev script, and an environment variable moves it.
Six apps run side by side, and they link to each other: the docs page for the playground opens the playground, the screenshot script drives the web app, the web app proxies the legacy app and reads the Go API. Every one of those is a URL that belongs to a different workspace.
The rule is that no app writes down another app's address. Ask the registry.
import { appOrigin, appUrl } from "statshub-tooling/apps";
appOrigin("api"); // http://localhost:8080
appUrl("playground", "/birds-eye-view"); // http://localhost:3004/birds-eye-view| Id | App | Environment variable |
|---|---|---|
web | statshub-web | WEB_ORIGIN |
admin | statshub-admin | ADMIN_ORIGIN |
api | statshub-api | API_ORIGIN |
docs | statshub-docs | DOCS_ORIGIN |
playground | statshub-playground | PLAYGROUND_ORIGIN |
legacy | legacy | LEGACY_ORIGIN |
The Expo app is absent on purpose: Metro serves a bundle over exp://, not a
page anything here can link to.
The port is read, not listed
Each app already states its port in its own dev script, and that is the only
copy that cannot drift. appPort reads it from there — the --port flag for a
Next app, the number handed to free-port.sh for one on Next's default, and
${PORT:-8080} inside scripts/dev.sh for the Go API, which has no framework
flag to carry it.
So moving an app is still a one-line change in that app, and nothing else has to be told.
Two ways to override, and they compose
DEV_HOST moves every app at once
Host only, ports untouched. DEV_HOST=box.tailb9cb87.ts.net makes all six
resolve to that machine, which is the whole answer when the dev box is
remote and a reader's browser is not on it.
<APP>_ORIGIN moves one, wholesale
Scheme, host and port. WEB_ORIGIN=https://statshub.com points everything
that links to web at production without touching anything that links to
the others. Set in a .env, a Docker build arg, or the shell.
A new variable has to be declared in turbo.json
Turbo runs tasks in strict environment mode, so a variable it has not been told
about never reaches the command — WEB_ORIGIN=… bun run dev:web sets nothing,
silently, and the app falls back to the default port. All seven live in
globalEnv, which both passes them through and puts them in the build cache
key. The cache key matters as much as the passthrough: these values are inlined
into the build output, so a build that ignored them would happily serve a cached
bundle with the wrong URLs baked in.
localhost is wrong more often than it looks
The dev box here is a remote server reached over Tailscale. A link to
localhost:3004 sends the reader to their own machine, where nothing is
serving. That is the failure DEV_HOST exists to fix, and it is why a
hardcoded http://localhost:... in a doc or a script is a bug rather than a
shortcut.
Using it
In the docs, <AppLink> and the app prop on <Feature> resolve at render —
so the same page is right in every environment it is read in.
<AppLink app="playground" path="/birds-eye-view">Bird's Eye View</AppLink>
<Feature icon="Map" title="Bird's eye view" app="playground" path="/birds-eye-view">
Every route in web, screenshotted.
</Feature>In a Next config or a script, call it directly. It is CommonJS, because the
next.config.js files are, and a second ESM copy for the TypeScript callers is
exactly the drift the registry exists to end.
const { appOrigin } = require("statshub-tooling/apps");
const API_ORIGIN = appOrigin("api");
const LEGACY_ORIGIN = appOrigin("legacy");To see what the current environment resolves to:
node packages/statshub-tooling/apps.cjsWhere state lives
On web the server fetches server state and passes it as props; the URL owns the rest, through nuqs. On mobile React Query owns server state and a zustand store owns the rest, because there is no URL to put it in.
Writing docs
The house style for this site — where a page lives, what shape it takes, which components are available, and what fails the build.