Add an app
A new workspace under apps/ — naming, turbo wiring, the root scripts, and the Docker stage that actually ships it.
An app is a deployable. If it is code two apps share, you want a package instead.
The workspace
apps/* is already globbed by the root package.json, so a new directory is
picked up with no config change. Name the package statshub-<dir>, the same string as the directory — the
directory name and the scoped name must agree, because every root script and
turbo filter addresses it by the scoped name.
{
"name": "statshub-reports",
"private": true,
"scripts": {
"dev": "next dev --port 3005",
"build": "next build",
"lint": "eslint .",
"typecheck": "tsc --noEmit"
}
}Pick a free port
Dev ports in use: legacy 3000, docs 3001, web/admin see their own scripts, Expo Metro 8142. Two apps on one port fail in a way that looks like a caching bug.
Then bun install from the repo root, once, to link it.
Turbo picks the tasks up by name
turbo.json declares build, lint, typecheck, test and dev. You do not
register an app — a script whose name matches a task is run by it. bun run lint
at the root now includes yours.
The one thing you DO have to declare is environment: build.env in turbo.json
is the cache key. A variable your build reads that is not in that list means
turbo serves a stale build when the value changes.
bunx turbo ls # confirm the workspace is seen
bunx turbo run typecheck --filter=statshub-reportsRoot scripts
Add the shortcuts next to their neighbours in the root package.json, following
the existing dev:* / build:* / start:* shape:
"dev:reports": "turbo run dev --filter=statshub-reports",
"build:reports": "turbo run build --filter=statshub-reports"Docker
Every deployable app has apps/<name>/Dockerfile built from the REPO ROOT as
context, and a service in docker-compose.yml. The first stage runs
turbo prune statshub-<name> --docker, which cuts the workspace down to that
app and its dependencies so a change elsewhere does not bust the layer cache.
Copy apps/statshub-docs/Dockerfile and change the filter — it is the smallest of them.
NEXT_PUBLIC_* values are baked at build time and must be passed as build args,
not runtime env.
Its pages in these docs
The sidebar's dropdown is the app switcher, and it is built from the content tree. An app that is not in it does not exist as far as this site is concerned.
apps/statshub-docs/content/docs/apps/reports/index.mdx # the overview
apps/statshub-docs/content/docs/apps/reports/meta.json # the section{
"title": "reports",
"description": "One line — it is the subtitle in the dropdown",
"icon": "ChartNoAxesCombined",
"root": true,
"pages": ["index", "features", "architecture"]
}root: true is what makes it an entry in the dropdown rather than a folder
nested in someone else's sidebar. index goes first in pages and its
frontmatter title is Overview — the dropdown already says which app you are
in, so repeating the name in the sidebar underneath adds nothing.
Then add the slug to content/docs/apps/meta.json, which is the order the apps
appear in. A root folder still needs its place in its parent's pages list:
drop apps from content/docs/meta.json and every app entry vanishes with the
subtree.
The bigger apps carry the same page names in the same order — Overview,
Features, Architecture, Data, Conventions — so one app can be read against
another. The front page says what belongs on each, and
Writing docs covers the shape of the overview
itself. Add a card to content/docs/index.mdx and a row to its ports table
while you are there.
Checklist
bun install
bunx turbo ls | grep reports
bun run typecheck
bun run lint
bun run build:reports
docker compose build reports
bun run build:docs # its page, and every icon name in it