A reusable, project-agnostic Turborepo + pnpm template for building TanStack single-page apps with an owned component library and Storybook.
It is scaffolding, not a product: the app is intentionally thin, there are no feature
screens, no auth, and no real data fetching. Clone it, swap the @acme namespace and
myapp name, swap the design tokens, and start building.
turbo-tanstack-starter/
├── apps/
│ └── myapp/ @acme/myapp — TanStack Router + Query SPA (Vite)
├── packages/
│ ├── ui/ @acme/ui — owned component library (source-only) + Storybook
│ ├── theme/ @acme/theme — design tokens (single source of truth)
│ └── config/ @acme/config — shared base tsconfig + Biome config
├── turbo.json — task graph (build / dev / lint / storybook) + caching
├── pnpm-workspace.yaml — workspace globs
└── package.json — root scripts (everything runs through Turbo)
| Package | Name | What it is |
|---|---|---|
apps/myapp |
@acme/myapp |
The app. App chrome (Header), routes, TanStack Router/Query wiring. |
packages/ui |
@acme/ui |
Hand-written, owned components (Button, Input, Table). Storybook here. |
packages/theme |
@acme/theme |
theme.css — the shadcn CSS-variable token set (--background, --primary, --radius, …). |
packages/config |
@acme/config |
tsconfig.base.json + biome/biome.base.json, extended by every package. |
@acme/ui is source we control — shadcn-style components that are hand-written,
composable, and live in the repo. They are not published to a registry and have
no build step. The app consumes them through a workspace symlink:
Because it's a symlink to source, editing a component in packages/ui/src is reflected
in the running app instantly — no rebuild, no version bump, no publish.
To add more primitives, run shadcn from the app — it's configured to install into the library, not the app:
cd apps/myapp
npx shadcn@latest add dialog # lands in packages/ui/src/components(apps/myapp/components.json points ui/utils/lib aliases at @acme/ui and css at
@acme/theme/theme.css.)
Tailwind here is v4 — there is no tailwind.config.js. Theming is CSS-based, and the
single source of truth is packages/theme/theme.css: it holds the token blocks
(:root / .dark) and the @theme inline mapping. Both the app and Storybook @import
it, so there is exactly one place that defines the design system.
Token values are intentionally neutral (the shadcn "zinc" default). Re-skinning a
project is a token-value swap, not a refactor — edit the values in theme.css and the
app and Storybook re-skin together.
Because @acme/ui ships raw source (no build), every Tailwind entry that renders its
components must point Tailwind at the library so its classes are generated. That's the
@source directive in apps/myapp/src/styles.css and
packages/ui/.storybook/preview.css:
@import "tailwindcss";
@import "@acme/theme/theme.css";
@source "../../../packages/ui/src"; /* scan owned components */- TanStack Router — type-safe, file-based routing with first-class search params as state. Kept deliberately (not React Router): the type safety and URL-as-state model are the point.
- TanStack Query — the server-state tool. It's the only state library here; no Redux/ Zustand/Jotai. Use Query for server state and Router search params for URL state.
This is a static single-page app. The Nitro/SSR adapter from the original scaffold has been
removed. pnpm build emits a plain static bundle deployable to any static host. (If a future
project needs SSR, add an adapter then — the template stays SPA.)
- Node 20+ (this repo is pinned to Node
24via.nvmrc; pnpm 11 itself requires Node ≥ 22.13) - pnpm (see
packageManagerin the rootpackage.json)
pnpm install
pnpm dev # runs apps/myapp at http://localhost:3000| Command | What it does |
|---|---|
pnpm dev |
Run apps/myapp (Vite dev server, port 3000). |
pnpm build |
Build every package/app (Turbo, cached). App: vite build && tsc. |
pnpm storybook |
Run Storybook for @acme/ui at http://localhost:6006. |
pnpm build-storybook |
Build the static Storybook (packages/ui/storybook-static). |
pnpm lint |
Biome lint across packages. |
pnpm format |
Biome format (write) across the repo. |
pnpm check |
Biome check --write across packages. |
pnpm typecheck |
tsc --noEmit across packages. |
You can scope any task to one package, e.g. pnpm --filter @acme/ui storybook.
Storybook is configured against packages/ui and is the primary surface for developing and
documenting components. It includes @storybook/addon-a11y so accessibility is a
first-class concern in the dev loop.
Shipped components (each with stories):
- Button — the prop-driven-variants example (
variant×sizevia class-variance-authority, plusasChildpolymorphism). - Input — text input with focus-ring and
aria-invalidstates. - Table — composable table shell (
Table,TableHeader,TableBody,TableRow,TableHead,TableCell,TableCaption).
pnpm storybook- TypeScript strict everywhere via
@acme/config/tsconfig.base.json(bundler resolution,verbatimModuleSyntax, no unused locals/params). - Biome only — formatting + linting (tabs, double quotes). No ESLint/Prettier.
Base config in
@acme/config/biome, extended per package. - Turborepo orchestrates tasks with
dependsOn/caching;dev/storybookare persistent and uncached.
routeTree.gen.tsis generated by the TanStack Router Vite plugin on dev/build and is gitignored — never edit it by hand.