StatsHub Docs

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
IdAppEnvironment variable
webstatshub-webWEB_ORIGIN
adminstatshub-adminADMIN_ORIGIN
apistatshub-apiAPI_ORIGIN
docsstatshub-docsDOCS_ORIGIN
playgroundstatshub-playgroundPLAYGROUND_ORIGIN
legacylegacyLEGACY_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.

apps/statshub-web/next.config.js
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.cjs

On this page