Skip to content
Documentation

Concepts

The mental model behind Stanza — apps, categories, vendoring, peers, and the manifest.

A handful of ideas explain how Stanza decides what to write and where. Once they click, the CLI's behavior is predictable.

Apps

A Stanza project contains one or more apps — each a buildable thing under apps/<dir>/ with its own framework and tests. An app is declared in stanza.json as:

{ "id": "web", "dir": "apps/web", "kind": "web" }
  • id — short, stable handle. Doubles as the workspace-package suffix (@<your-app>/web) and as the value you pass to --app=<id> in the CLI.
  • dir — repo-relative directory the app lives in.
  • kindweb or native. Lets Stanza reject obviously-wrong installs (e.g. installing Next.js into a kind: "native" app).

stanza init scaffolds a single web app today ({ id: "web", dir: "apps/web", kind: "web" }). Multi-app projects — a web app and an Expo native app sharing a packages/db/, for example — are on the roadmap, and the schema is already shaped for them. App-home module records carry an apps: ["<id>"] field so add/remove know which app to target; package-home modules ship their core code once and route shims into the apps that consume them.

Categories

Every module belongs to exactly one category. A category has two independent properties that govern how its modules behave:

  • Cardinalityone (single-choice) or many (coexisting).
  • Home — where a module's output lands: app, repo, or package.
CategoryCardinalityHome
frameworkoneapp
uionepackage
dbonepackage
ormonepackage
authonepackage
toolingonerepo
testingmanyapp

A one category holds at most one module — adding a second fails until you remove the first. For app-home categories that limit is per app: a project with two apps could pick next for the web app and expo for the native one. A many category lets modules coexist (e.g. Vitest and Playwright side by side).

Home decides placement:

  • app modules wire a specific app's shell (in apps/web/, say). They install into the app(s) named in the module record's apps field.
  • repo modules write config at the monorepo root.
  • package modules install into their own internal workspace package under packages/<dir>/, named @<your-app>/<dir>, which every consuming app depends on via workspace:*. db and orm share a single packages/db/ so the ORM client sits next to the schema it queries.

framework is one category, not many. A module's appKind (web or native) pins it to a platform — Next is appKind: "web", a future Expo module would be appKind: "native". Splitting the category per platform would force every peer-aware category (ui, testing, …) to split too, which would be a worse design. See the registry for the roadmap.

The table above is a representative subset. The full taxonomy also includes ai, payments, email, and monorepo (single-choice) and deploy (coexisting) — most with modules available today — plus api (single-choice), defined with its modules on the roadmap. See the module registry for the complete list and per-module status.

Vendoring

Stanza copies a module's code into your repo verbatim. There's no shared runtime package — the generated files are ordinary source you can read, edit, and commit. That means upgrades never silently change your app's behavior, and you're never locked into Stanza's abstractions.

Peers and adapters

Modules declare peers — the other categories they care about. When you add a module, Stanza reads your current selections and picks the adapter that matches. Better Auth, for instance, peers on both framework and orm, so adding it to a Next + Drizzle app produces different wiring than a TanStack Start + Prisma app. You pick the module; Stanza picks the right variant.

Peers are scoped to the app being targeted: in a multi-app project, the ui module's adapters peer-match against the active app's framework. That's how the same Tailwind module can ship a postcss.config.mjs for a Next.js web app while a future NativeWind module ships tailwind.config.js for an Expo native app.

The manifest (stanza.json)

The stanza.json file at your repo root is the source of truth for what's installed:

{
  "$schema": "https://stanza.tools/schema.json",
  "version": "0.4",
  "projectShape": "monorepo",
  "packageManager": "pnpm",
  "name": "acme",
  "apps": [{ "id": "web", "dir": "apps/web", "kind": "web" }],
  "modules": {
    "framework": [{ "id": "next", "version": "0.1.0", "adapter": "default", "apps": ["web"] }],
    "db": [{ "id": "postgres", "version": "0.1.0", "adapter": "default" }],
    "auth": [
      { "id": "better-auth", "version": "0.1.0", "adapter": "next+drizzle", "apps": ["web"] }
    ],
    "tooling": [{ "id": "oxlint-oxfmt", "version": "0.1.0", "adapter": "default" }]
  },
  "regions": {
    /* … */
  }
}

add, remove, and list all read it. Treat it as generated state: don't hand-edit it unless you're repairing something deliberately.

A module record's apps field tells Stanza which app(s) the install targets:

  • Required for home: "app" modules (framework, testing). One record per app.
  • Optional for home: "package" modules. Omitted means "ship app-scoped shims into every app"; an explicit list restricts to those apps. The package itself is always written once under packages/<dir>/.
  • Forbidden for home: "repo" modules — they're project-wide.

Regions

A region is a claim on a file (or a section of one) by a specific module. This is how stanza remove knows exactly what to delete and what to leave alone: it sweeps only the regions the removed module owns. Because regions key on the module, two modules can safely write disjoint parts of the same file — Vitest owning the test script and Playwright owning test:e2e, for example, without conflict. Per-app writes live at distinct paths (apps/web/page.tsx vs apps/native/app/(tabs)/index.tsx), so they never collide either.

Open registry

A registry is just static JSON: an index plus one file per module, each carrying its templates, dependencies, env vars, and codemod invocations. The CLI ships with a default registry under the @stanza namespace, and you can publish your own modules under any @scope you want and consume them alongside the first-party ones — see Third-party registries for the manifest field and the @ns/id CLI syntax. The full module + registry surface is documented in the Authoring manual.