- Introduce a `kind: "slot" | "addon"` discriminated union on `Module` — slot modules carry `slot`, add-on modules carry `category` (one of `KNOWN_ADDONS`). `isAddon()` narrows the union; `moduleGroup()` returns the slot-or-category key. Add-on categories (`testing`, `tooling`, `deploy`, `email`, `monorepo`) live in `KNOWN_ADDONS`/`ADDON_CATEGORIES`, deliberately disjoint from `KNOWN_SLOTS` so the resolver never lets add-ons participate in peer dispatch (they can still declare a one-way `peers` + per-framework adapters)
- Ship the first two add-ons: `testing-vitest` (jsdom + RTL, `test`/`test:watch` scripts, per-framework adapters) and `testing-playwright` (per-framework `webServer`, `test:e2e`/`test:e2e:ui` scripts — disjoint from vitest's so region claims don't collide); both under `registry/modules/testing-*/`
- Manifest gains `addons: Partial<Record<AddonCategoryId, StanzaAddonRecord[]>>` (`.default({})` keeps existing `stanza.json` valid); resolver gains `addonOrder` for deterministic codemod sequencing
- CLI: `stanza add <slot|category> <module>` accepts both slots and add-on categories; duplicate-id guard for add-ons instead of the single-fill slot guard; `stanza remove <slot|category> [id]` removes a specific add-on by id (or the whole slot); `stanza list` shows add-ons alongside slots; `stanza init --yes` gains `--testing vitest,playwright` flag; wizard prompts multi-select for add-on categories
- Web builder: add-on slots render as checkbox multi-select cards (`SlotSection` detects `isAddon` and switches to `<CheckboxCard>`), `?testing=vitest,playwright` comma-joined URL param, `builder-state.functions.ts` parses/serializes per-category add-on lists, OG and sitemap routes updated for add-on module paths
- Codemod runner branches on `isAddon` to resolve add-on records from `manifest.addons[category]` rather than `manifest.modules[slot]`; region ownership keys on `module.id` so co-existing add-ons writing the same file at disjoint dot-paths never conflict
- Add `commands.test.ts`, `selection.test.ts`, `manifest.test.ts`, and `resolver.test.ts` coverage for add-on add/remove/list flows and schema validation
6.6 KiB
First-party module registry
This is the canonical roadmap for the first-party modules stanza ships. Each
entry maps to a registry/modules/<slot>-<id>/ directory. Update this file
when a module lands, gets renamed, or is dropped.
Legend: [x] added · [ ] planned
Categories: slots vs add-ons
Stanza's module taxonomy splits into two categories. Slots are single-choice and constrain other modules' adapter dispatch (picking Next.js influences which auth adapters are available). Add-ons can coexist freely and don't influence anyone else's adapters (vitest doesn't constrain anything).
| Category | Type | Examples |
|---|---|---|
| Slots (constraint-bearing, one choice each) | framework, styling, db, orm, auth, api, ai, ui, payments |
next vs tanstack-start; drizzle vs prisma |
| Add-ons (no constraints, many allowed) | testing, tooling, deploy, email, monorepo |
vitest + playwright together; eslint or biome |
The add-on schema is live as of the testing modules. Add-ons are
modeled with a kind: "addon" discriminator on Module (carrying a
category instead of a slot) and a manifest.addons record keyed by
category, each holding a list of records (Partial<Record<AddonCategoryId, StanzaAddonRecord[]>>). Add-on categories live in KNOWN_ADDONS /
ADDON_CATEGORIES, deliberately disjoint from KNOWN_SLOTS so they never
participate in peer resolution — yet they can still declare a one-way
peers (e.g. { framework: [...] }) and framework-varying adapters.
framework
- tanstack-start — TanStack Start on Vite (no Vinxi). Provides
web,react,ssr,node - next — Next.js 16 (App Router). Provides
web,react,ssr,rsc,node,edge - nuxt — Vue. Will require relaxing the React-implicit assumption in many peer modules (capability tag
vue) - svelte — SvelteKit. Capability
svelte - solid — SolidStart. Capability
solid
api
New slot (single-choice, constraint-bearing). Optional layer between the framework and the database/services.
- trpc — tRPC v11; per-framework adapters (next, tanstack-start, nuxt, svelte, solid)
- orpc — oRPC
ai
New slot (single-choice, constraint-bearing).
- vercel-ai-sdk —
aipackage + provider sub-recipes - tanstack-ai — TanStack AI
auth
- better-auth — headless, peers
orm: [drizzle, prisma],framework: [next, tanstack-start] - clerk — hosted UI, peers
framework: [next](TanStack Start adapter planned) - workos — WorkOS AuthKit
orm
- drizzle — Drizzle ORM 0.45, peers
db: [postgres, sqlite] - prisma — Prisma 7, peers
db: [postgres, sqlite]
db
- postgres —
postgresdriver 3.4.x - sqlite —
better-sqlite312.x
styling
- tailwind — Vanilla Tailwind v4, adapters per framework
- shadcn-radix — classic shadcn/ui (Radix primitives)
- shadcn-base — shadcn on react-base-ui
payments
New slot (single-choice, constraint-bearing).
- stripe — Checkout Sessions + webhooks
- polar — Polar SDK
New add-on (multi-allowed, no constraints).
- resend — Resend SDK + React Email templates
tooling
New add-on. Lint/format toolchain. Conventionally one per project but doesn't constrain others.
- eslint-prettier — ESLint + Prettier
- biome — Biome (lint + format)
- oxlint-oxfmt — Oxlint + oxfmt
testing
New add-on. Vitest and Playwright are independent and routinely coexist.
- vitest — unit + integration; per-framework adapters (next, tanstack-start),
jsdom+ RTL,test/test:watchscripts - playwright — e2e; per-framework
webServer(next dev/vite dev),test:e2e/test:e2e:uiscripts (disjoint from vitest's)
deploy
New add-on.
- vercel —
vercel.json+ framework-specific output - cloudflare — Workers / Pages adapter per framework
- railway —
railway.toml+ Dockerfile - docker — generic
Dockerfile+ compose for self-host
monorepo
Currently hardcoded in bootstrapShell as Turborepo. Promotes to an add-on when a second option (Nx, Moonrepo) lands.
- turborepo — Turbo 2.x (current default; not yet a configurable choice)
packageManager
Not a slot — a top-level field in stanza.json (packageManager: "pnpm" | "bun" | "npm"). Wizard prompts; codemods only touch package.json, never lockfiles.
- pnpm (default)
- bun
- npm
- yarn — needs lockfile/workspace handling that differs from the others
Slot-package extraction
Generated projects place each slot's output in one of two homes:
- App-scoped (
framework,styling) — files land inmanifest.appDir(e.g.apps/web/). These slots wire the app shell itself, so there's no useful extraction boundary. - Package-scoped (
auth,db,orm) — files land inpackages/<dir>/, named@<manifest.name>/<dir>, and the app gets aworkspace:*dep.dbandormshare a singlepackages/db/package so the ORM client sits next to the schema it queries.
The mapping lives in the canonical SLOTS array as the packageDir field — SLOT_PACKAGE_DIR is just a Record<SlotId, ...> view derived from it. When you add a new slot, decide upfront whether it extracts (data layer, observability, payments) or wires the shell (router, UI primitives that mount in <html>) and set packageDir accordingly.
Slot taxonomy changes required
Adding a new slot is now a two-line edit: append the id to KNOWN_SLOTS (the as const tuple Zod needs) and append a Slot entry to SLOTS with { id, label, description, packageDir }. Order is topological — earlier slots become peer candidates for later ones. Existing stanza.json files don't break: new slots are optional, so adding them is additive.
Adding an add-on category is the same two-line edit against KNOWN_ADDONS + ADDON_CATEGORIES. Authoring an add-on module sets kind: "addon" + category (instead of slot) on defineModule; the manifest's addons record and the runner/CLI/web surfaces already handle multi-choice. See the category table at the top of this file for which planned modules are add-ons vs slots.