A CLI tool for scaffolding out a new Next.js application with @schemavaults/auth, @schemavaults/theme, and @schemavaults/dbh configured.
Every scaffolded app also ships a typed HTTP API layer built on @schemavaults/openapi-operations: each route under src/app/api/ is defined once (zod request/response schemas, auth requirements, handler) and served as its own Hono app from its Next.js route.ts. public/openapi.json is generated from the same definitions on every bun run dev / bun run build (git-ignored; bun run openapi:check verifies the route files against the catalogue in lint and CI) and served at /openapi.json; /docs renders the document with @schemavaults/openapi-docs-ui. An api-routes Claude Code skill in the app's .claude/skills/ explains how to add routes so they are registered in the document.
When initializing a project, the CLI also installs the @schemavaults/dbh database-migrations Claude Code skill into the new project's .claude/skills/ (via npx skills add), so coding agents know how to author migrations in the format this template scaffolds. It additionally scaffolds a nextjs-docs skill that points coding agents at the version-matched Next.js documentation bundled with the installed next package (node_modules/next/dist/docs/) instead of web searches or memory.
Scaffolded apps also ship Claude Code skills for coding conventions: commit-changes (raise the app's
package.json version by semver on every branch), small-modules (keep files under about 200
lines; the app's eslint config warns above 200 lines and fails above 350),
react19-no-forward-ref (pass ref as a prop) and react19-use-transition (track pending form
submissions with useTransition instead of a manual loading flag).
Apps that need blob storage can opt in with --blob-storage s3: they get a pre-configured S3 client and an S3-compatible object store (RustFS) in docker-compose.yml for local development.
Each deployment can re-theme its app without code changes: THEME_* environment variables (THEME_LIGHT_PRIMARY, THEME_DARK_PRIMARY, THEME_RADIUS, ...) override @schemavaults/theme's tokens on every root document (layout.tsx, global-not-found.tsx and global-error.tsx); see the "Theme" section of the generated app's README.md. The error pages show the app's display name, in the theme's accent gradient, instead of the SchemaVaults wordmark.
Use the latest version of @schemavaults/init-next-app published to NPM:
npx @schemavaults/init-next-app my-new-app-nameAny value not provided via a flag is requested via an interactive stdin prompt.
For scripts and CI where stdin prompts are not possible, pass every value as a flag:
npx @schemavaults/init-next-app my-new-app-name \
--display-name "My New App" \
--description "A short description of my new app" \
--client-app-id "my-new-app" \
--api-server-id "my-api-server" \
--auth-server-url "https://auth.schemavaults.com" \
--auth-server-app-id "schemavaults-auth" \
--deployment "vercel" \
--blob-storage "s3"| Flag | Description |
|---|---|
--display-name <name> |
Human-readable project name. |
--description <text> |
Project description. |
--client-app-id <id> |
SCHEMAVAULTS_CLIENT_APP_ID written to .env.local. Validated with appIdSchema from @schemavaults/app-definitions: 2-64 characters of lowercase alphanumerics, hyphens, and underscores, starting with an alphanumeric and not ending with a hyphen or underscore (UUIDs remain valid). |
--api-server-id <id> |
SCHEMAVAULTS_API_SERVER_ID written to .env.local. Validated with apiServerIdSchema from @schemavaults/app-definitions (same format as --client-app-id). |
--auth-server-url <url> |
SCHEMAVAULTS_AUTH_SERVER_URL written to .env.local (must be an http(s) URL). Defaults to https://auth.schemavaults.com; set this to point the app at a self-hosted auth server, e.g. https://auth.acmecorp.com. When prompted interactively, press enter to accept the default. |
--auth-server-app-id <id> |
SCHEMAVAULTS_AUTH_SERVER_APP_ID written to .env.local and .env.example: the auth server's own app id (validated like --client-app-id). When prompted interactively, the CLI first fetches the id the auth server publishes at <auth-server-url>/api/config/app-id and offers it as the default (press enter to accept it); if that request fails (e.g. an auth server that predates the endpoint), it prints a warning and offers schemavaults-auth instead. |
--deployment <strategy> |
vercel or none. With vercel, the app also gets a vercel.json, a publish-to-vercel job in .github/workflows/ci.yml, and VERCEL_* entries in .env.example. |
--blob-storage <provider> |
s3 or none; when prompted interactively, press enter for none. With s3, the app also gets src/lib/s3.ts (a shared @aws-sdk/client-s3 client configured from S3_* environment variables, for AWS S3 or any S3-compatible provider), an s3 service in docker-compose.yml running RustFS with a dev-bucket bucket that .env.local points at, S3_* entries in .env.example, and a blob-storage Claude Code skill. |
The generated application lives in this repository as a real Next.js project,
templates/schemavaults-next-app/, and is rendered by
@jalexw/mould. Every file you see there is copied to the new
project as-is, apart from:
- Placeholders such as
xxx_project_name_xxxandxxx_display_name_xxx, which are replaced with the values you pass. They are lowercase, identifier-safe tokens, so the template itself is a valid npm package name / env value / JSX text and installs and type-checks unmodified. The auth server URL is substituted from its literal defaulthttps://auth.schemavaults.com. - Conditional blocks wrapped in
# mould:if deployment == vercel…# mould:endifcomment lines (.github/workflows/ci.yml,.env.example), andvercel.json, which is only copied when--deployment vercelis chosen. Likewiseblob_storage == s3blocks (docker-compose.yml,_env.local,.env.example,README.md), andsrc/lib/s3.tsand theblob-storageskill, which are only copied with--blob-storage s3. _gitignoreand_env.local, written to the new project as.gitignoreand.env.local. npm would otherwise rename a real.gitignoreinside the published package, and keeping every.env*(other than.env.example) out of the template means a real env file can never be committed or shipped by accident.@schemavaults/*versions inpackage.json, which the CLI bumps to the latest published versions after rendering; the template pins real versions so it installs on its own.- Optional dependencies in
package.json. JSON cannot hold conditional blocks, so the template always depends on@aws-sdk/client-s3(and so type-checkssrc/lib/s3.ts); the CLI removes it unless--blob-storage s3is chosen.
The mapping is declared in .mouldconfig.json.
After rendering, the CLI runs bun install, bun run auth-codegen and installs the
database-migrations Claude skill, as before.
bun installbun run build
node ./dist/index.js test-appWork on the template like on any other Next.js app — with type checking, eslint and your editor's tooling:
cd templates/schemavaults-next-app
bun install
bun run typecheck # runs auth-codegen first; its output is git-ignored and never shipped
bun run lint # also checks the API route files against the operations catalogue
bun run openapi:generate # writes the git-ignored public/openapi.json (dev and build do this too)To run the template itself, copy _env.local to .env.local first; every .env* except
.env.example is ignored by git and excluded from the published package.
Rules of thumb:
- Never add a
.gitignoreor a.env*file inside the template; edit_gitignore/_env.localfor the generated app and the repository root.gitignorefor development artefacts. - New placeholders must be declared under
substitutionsin.mouldconfig.json; new build artefacts must be listed in three places:ignorePatternsin.mouldconfig.json(so mould never copies them), the root.gitignore(so git never tracks them), and the negatedfilesentries in the rootpackage.json(sonpm packnever ships them — a checkout where the template was just installed would otherwise put itsnode_modules/in the tarball). scripts/test-template.shchecks all of the above and that the template type-checks.
CI publishes the package to npm on every push to main, and npm rejects a version that is already
published. Every branch must therefore raise version in package.json by semver once before it is
merged. While the version is 0.x, breaking CLI changes bump minor and everything else bumps patch.
bun run check:version (also the version job in CI, on every pull request) fails when the version
is already published or not above npm's latest. The repository's commit-changes Claude Code skill
(.claude/skills/commit-changes/) walks coding agents through it.
bun run test # end-to-end: pack, scaffold test-app from the tarball, install, typecheck, lint, build
bun run test none none # the same with --deployment none --blob-storage none (defaults: vercel s3)
bun run test:template # the template directory itself installs, type-checks and lints