# Concepts (/docs/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 [#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:

```json
{ "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.
* **`kind`** — `web` 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 [#categories]

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

* **Cardinality** — `one` (single-choice) or `many` (coexisting).
* **Home** — where a module's output lands: `app`, `repo`, or `package`.

| Category    | Cardinality | Home    |
| ----------- | ----------- | ------- |
| `framework` | one         | app     |
| `ui`        | one         | package |
| `db`        | one         | package |
| `orm`       | one         | package |
| `auth`      | one         | package |
| `tooling`   | one         | repo    |
| `testing`   | many        | app     |

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.

<Callout type="info">
  **`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](/docs/registry) for the roadmap.
</Callout>

<Callout type="info">
  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](/docs/registry) for the complete list and per-module status.
</Callout>

## Vendoring [#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 [#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-manifest-stanzajson]

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

```json
{
  "$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 [#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 [#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](/docs/registry#third-party-registries) for the manifest field and the `@ns/id` CLI syntax. The full module + registry surface is documented in the [Authoring manual](/docs/authoring).
