— kebab-lowercase, no `@` or `/`.\n\n## Minimum viable template\n\n```text\nmy-template/\n├── template.json\n├── package.json\n├── app.config.ts\n├── tsconfig.json\n└── src/\n └── pages/\n └── index.tsx\n```\n\nThe `template.json` manifest (`id`, `kind`, `summary`, optional `tags`):\n\n```json\n{\n \"id\": \"acme-internal-tool\",\n \"kind\": \"web\",\n \"summary\": \"Internal tool starter — our team's auth + table conventions baked in.\",\n \"tags\": [\"internal\", \"tool\"]\n}\n```\n\n`kind` is `'api'` or `'web'` — the CLI rejects an api template passed to `--web` (and vice-versa). `id` must equal the directory name, or the loader skips the dir.\n\nNow `voltro list-templates` includes it, and `voltro create-project x --web=acme-internal-tool` (web) or `--api=acme-internal-tool` (api) scaffolds it.\n\n## Token substitution\n\nAny file's CONTENT can contain placeholders. The scaffolder rewrites them from the app + project names you pass; the kebab form is what's persisted (folders, `package.json` `name`), camel/Pascal/snake are derived per use:\n\n| Token | Form | Example (`appName=my-app`, `projectName=acme-cms`) |\n|---|---|---|\n| `{{appName}}` | kebab | `my-app` |\n| `{{projectName}}` | kebab | `acme-cms` |\n| `{{appNameCamel}}` | camelCase | `myApp` |\n| `{{projectNameCamel}}` | camelCase | `acmeCms` |\n| `{{appNamePascal}}` | PascalCase | `MyApp` |\n| `{{projectNamePascal}}` | PascalCase | `AcmeCms` |\n| `{{capAppName}}` | PascalCase alias of `appNamePascal` | `MyApp` |\n| `{{capProjectName}}` | PascalCase alias of `projectNamePascal` | `AcmeCms` |\n| `{{appNameSnake}}` | snake_case (SQL / bucket names) | `my_app` |\n| `{{projectNameSnake}}` | snake_case | `acme_cms` |\n| `{{port}}` | the port allocated from the project's `portRange` (web apps) | `5191` |\n\nExample `app.config.ts`:\n\n```ts\nexport default {\n type: 'web' as const,\n name: '{{capProjectName}}{{capAppName}}',\n port: {{port}},\n}\n```\n\nSubstitution is global across all files (content only — file NAMES aren't templated). The scaffolder also rewrites `package.json`'s `name` field on the fly so the scaffolded app gets a real pnpm-workspace name even if the template forgot to template it.\n\n## Where templates live\n\nThe CLI loads templates from two places, in order:\n\n1. **Source mode** (dev): walks up from the CLI's own dir looking for a sibling `voltro-templates/apps/`. This is the source of truth in this repo.\n2. **Bundled fallback** (post-publish): a snapshot of `voltro-templates/apps/` shipped inside the CLI package.\n\nTo add a template for the framework itself, drop a directory under `voltro-templates/apps/\u003cid>/`. There is no env-var path override and no npm-package fetch — the loader is hardcoded to those two locations.\n\n## What's in a template\n\n| File | Purpose |\n|---|---|\n| `template.json` | Manifest with `id`, `kind`, `summary`, optional `tags`. Stripped — never lands in the scaffolded app. |\n| `package.json` | The scaffolded app's deps + scripts. Its `name` is rewritten on scaffold. |\n| `app.config.ts` | The app's framework config (port, name, plugins). |\n| `tsconfig.json` | TypeScript config. Usually extends a workspace base. |\n| `README.md` | Human-readable description; copied verbatim (with tokens substituted). |\n| everything else | Copied into the target app dir with token substitution. |\n\nThe scaffolder copies every file except `template.json` (and obvious build artefacts — `node_modules/`, `dist/`, `.framework/`). No \"templating engine\" beyond the token-replacement walker.\n\n## Tips for good templates\n\n- **Start small.** A short index page + the right deps beats a 2000-line \"everything imaginable\".\n- **Inline comments explain the WHY.** Scaffolded code is read by the next dev. The shipped templates' comments are part of the value.\n- **Use the kit primitives.** Don't re-invent a Button when `@voltro/ui-shadcn` ships one.\n- **Leave room.** Show the pattern; don't fill every file with example data. The user will replace it.\n- **Test by scaffolding.** Before relying on it, `voltro create-project --web=my-template` against a clean directory. Boot it. Refine.\n\n## The framework's own templates\n\nRead `voltro-templates/apps/*/` for reference. Every shipped template's source is the best example of \"what a good template looks like\" — short, opinionated, idiomatic Voltro.\n\n## Listing templates\n\n```bash\nvoltro list-templates\nvoltro list-templates --json\n```\n\nFor programmatic use (build + publish a catalogue page):\n\n```ts\nimport { listTemplates } from '@voltro/cli'\nconst all = await listTemplates()\n// [{ id, kind, summary, tags, path }, …]\n```\n\n## See also\n\n- [Scaffolding CLI](/docs/cli/scaffolding) — the consumer of templates.\n- [App templates overview](/docs/templates/overview) — the shipped catalogue.\n"},"segments":{"0":{"toc":[{"id":"minimum-viable-template","text":"Minimum viable template","level":2},{"id":"token-substitution","text":"Token substitution","level":2},{"id":"where-templates-live","text":"Where templates live","level":2},{"id":"what-s-in-a-template","text":"What's in a template","level":2},{"id":"tips-for-good-templates","text":"Tips for good templates","level":2},{"id":"the-framework-s-own-templates","text":"The framework's own templates","level":2},{"id":"listing-templates","text":"Listing templates","level":2},{"id":"see-also","text":"See also","level":2}]}},"ran":{"page":true,"segments":[0]}}

Roll your own

Add a template the CLI can scaffold from — a directory under voltro-templates/apps/ with a manifest and the files to copy.

Templates aren't magic. Each one is a directory under voltro-templates/apps/<id>/ with a template.json manifest plus the files to scaffold. Drop a new directory and the CLI walks it on the next voltro list-templates.

The directory NAME is the template id you pass to --api / --web / --template. The id must match ^[a-zA-Z][a-zA-Z0-9-]*$ — kebab-lowercase, no @ or /.

Minimum viable template

my-template/
├── template.json
├── package.json
├── app.config.ts
├── tsconfig.json
└── src/
    └── pages/
        └── index.tsx

The template.json manifest (id, kind, summary, optional tags):

{
  "id":      "acme-internal-tool",
  "kind":    "web",
  "summary": "Internal tool starter — our team's auth + table conventions baked in.",
  "tags":    ["internal", "tool"]
}

kind is 'api' or 'web' — the CLI rejects an api template passed to --web (and vice-versa). id must equal the directory name, or the loader skips the dir.

Now voltro list-templates includes it, and voltro create-project x --web=acme-internal-tool (web) or --api=acme-internal-tool (api) scaffolds it.

Token substitution

Any file's CONTENT can contain placeholders. The scaffolder rewrites them from the app + project names you pass; the kebab form is what's persisted (folders, package.json name), camel/Pascal/snake are derived per use:

Token Form Example (appName=my-app, projectName=acme-cms)
{{appName}} kebab my-app
{{projectName}} kebab acme-cms
{{appNameCamel}} camelCase myApp
{{projectNameCamel}} camelCase acmeCms
{{appNamePascal}} PascalCase MyApp
{{projectNamePascal}} PascalCase AcmeCms
{{capAppName}} PascalCase alias of appNamePascal MyApp
{{capProjectName}} PascalCase alias of projectNamePascal AcmeCms
{{appNameSnake}} snake_case (SQL / bucket names) my_app
{{projectNameSnake}} snake_case acme_cms
{{port}} the port allocated from the project's portRange (web apps) 5191

Example app.config.ts:

export default {
  type: 'web' as const,
  name: '{{capProjectName}}{{capAppName}}',
  port: {{port}},
}

Substitution is global across all files (content only — file NAMES aren't templated). The scaffolder also rewrites package.json's name field on the fly so the scaffolded app gets a real pnpm-workspace name even if the template forgot to template it.

Where templates live

The CLI loads templates from two places, in order:

  1. Source mode (dev): walks up from the CLI's own dir looking for a sibling voltro-templates/apps/. This is the source of truth in this repo.
  2. Bundled fallback (post-publish): a snapshot of voltro-templates/apps/ shipped inside the CLI package.

To add a template for the framework itself, drop a directory under voltro-templates/apps/<id>/. There is no env-var path override and no npm-package fetch — the loader is hardcoded to those two locations.

What's in a template

File Purpose
template.json Manifest with id, kind, summary, optional tags. Stripped — never lands in the scaffolded app.
package.json The scaffolded app's deps + scripts. Its name is rewritten on scaffold.
app.config.ts The app's framework config (port, name, plugins).
tsconfig.json TypeScript config. Usually extends a workspace base.
README.md Human-readable description; copied verbatim (with tokens substituted).
everything else Copied into the target app dir with token substitution.

The scaffolder copies every file except template.json (and obvious build artefacts — node_modules/, dist/, .framework/). No "templating engine" beyond the token-replacement walker.

Tips for good templates

  • Start small. A short index page + the right deps beats a 2000-line "everything imaginable".
  • Inline comments explain the WHY. Scaffolded code is read by the next dev. The shipped templates' comments are part of the value.
  • Use the kit primitives. Don't re-invent a Button when @voltro/ui-shadcn ships one.
  • Leave room. Show the pattern; don't fill every file with example data. The user will replace it.
  • Test by scaffolding. Before relying on it, voltro create-project --web=my-template against a clean directory. Boot it. Refine.

The framework's own templates

Read voltro-templates/apps/*/ for reference. Every shipped template's source is the best example of "what a good template looks like" — short, opinionated, idiomatic Voltro.

Listing templates

voltro list-templates
voltro list-templates --json

For programmatic use (build + publish a catalogue page):

import { listTemplates } from '@voltro/cli'
const all = await listTemplates()
// [{ id, kind, summary, tags, path }, …]

See also