create-basalt 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -48,6 +48,7 @@ Subscriptions / billing? (y/N) n
48
48
  Web UI (React + shadcn)? (y/N) n
49
49
  MCP server (routes as AI-agent tools)? (y/N) n
50
50
  'basalt' CLI (code generators)? (y/N) n
51
+ Database (PostgreSQL via Prisma)? (y/N) n
51
52
  Install dependencies now? (y/N) y
52
53
  Initialize a git repository? (y/N) y
53
54
  ```
@@ -91,6 +92,7 @@ Usage: npm create basalt <name> [options]
91
92
  | `--ui` | off | Generates the `web/` frontend (React + shadcn via `@basaltkit/admin-shadcn` + `@basaltkit/sdk`). **Forces pnpm** — see note below |
92
93
  | `--cli` | off | Generates the `basalt` CLI (`bin/basalt.ts`, `pnpm basalt` script, `make:*` generators from `@basaltkit/generator`) |
93
94
  | `--mcp` | off | Exposes read-only routes as MCP tools (`@basaltkit/mcp`) over HTTP at `POST /mcp` — the overview and health endpoints are opted in via `meta.mcp` |
95
+ | `--prisma` (alias `--db`) | off | Backs the app with PostgreSQL through Prisma: `prisma/schema.prisma`, `prisma.config.ts`, `src/db.ts`, the `@basaltkit/*-prisma` stores instead of the in-memory ones, a required `<APP>_DATABASE_URL`, the `db:*` scripts and `prismaPlugin({ assertMigrated: true })` — see *With `--prisma`* below |
94
96
  | `--install` | off | Installs dependencies at the end (with the detected/chosen manager) |
95
97
  | `--git` | off | Runs `git init` + first commit ("Initial commit from create-basalt") |
96
98
  | `--offline` | off | Don't query the npm registry for the latest versions; use the ranges bundled with this create-basalt release |
@@ -115,7 +117,7 @@ Behavior notes (faithful to the code):
115
117
  pnpm create basalt
116
118
 
117
119
  # Full project, no questions, with everything:
118
- pnpm create basalt my-app --billing --ui --cli --install --git
120
+ pnpm create basalt my-app --billing --ui --cli --prisma --install --git
119
121
 
120
122
  # Minimal API (no tenancy or auth), in another folder:
121
123
  npm create basalt service-api --no-tenancy --no-auth --dir=./apps/service-api
@@ -135,19 +137,19 @@ Always:
135
137
  my-app/
136
138
  ├── package.json # scripts: dev, start, test, typecheck (+ basalt with --cli)
137
139
  ├── tsconfig.json # strict TypeScript, ESM
138
- ├── .env.example # PORT, HOST, LOG_LEVEL, NODE_ENV (+ APP_SECRET with auth) + the --env-file precedence warning
140
+ ├── .env.example # MY_APP_PORT, MY_APP_HOST, MY_APP_LOG_LEVEL, NODE_ENV (+ MY_APP_APP_SECRET with auth) + the --env-file precedence warning
139
141
  ├── .gitignore
140
142
  ├── README.md # instructions adapted to the chosen options
141
143
  ├── pnpm-workspace.yaml # esbuild allowBuilds, @basaltkit/* release-age exclusion (+ "web" member with --ui) + pnpm 11 notes
142
144
  ├── src/
143
- │ ├── env.ts # environment variables validated with Zod (@basaltkit/env)
145
+ │ ├── env.ts # environment variables validated with Zod (@basaltkit/env), app-prefixed
144
146
  │ ├── app.ts # buildApp() with the chosen plugins
145
147
  │ ├── routes.ts # GET / (friendly index) and GET /health
146
148
  │ └── server.ts # startup + clean shutdown on SIGINT/SIGTERM
147
149
  └── tests/app.test.ts # smoke test adapted to the options
148
150
  ```
149
151
 
150
- With `--cli`, adds `bin/basalt.ts` and the `"basalt": "tsx bin/basalt.ts"` script. With `--ui`, adds the `web/` folder (Vite 8 + React 19 + Tailwind CSS 4 via `@tailwindcss/vite` + shadcn — Tailwind is configured in `web/src/index.css`, no `tailwind.config.js`/PostCSS — with `web/src/api.ts` built on top of `@basaltkit/sdk`; with auth on it includes a login/register screen).
152
+ With `--prisma`, adds `prisma/schema.prisma`, `prisma.config.ts`, `src/db.ts`, `prisma/seed.ts` (with tenancy) and the `db:generate` / `db:migrate` / `db:deploy` / `db:seed` scripts. With `--cli`, adds `bin/basalt.ts` and the `"basalt": "tsx bin/basalt.ts"` script. With `--ui`, adds the `web/` folder (Vite 8 + React 19 + Tailwind CSS 4 via `@tailwindcss/vite` + shadcn — Tailwind is configured in `web/src/index.css`, no `tailwind.config.js`/PostCSS — with `web/src/api.ts` built on top of `@basaltkit/sdk`; with auth on it includes a login/register screen).
151
153
 
152
154
  ### With `--ui`: running the API and frontend
153
155
 
@@ -159,6 +161,44 @@ pnpm --filter my-app-web dev # terminal 2 — UI at http://localhost:5180
159
161
 
160
162
  Vite's dev server proxies `/api` to the API — there's no CORS to configure.
161
163
 
164
+ ### With `--prisma`: a PostgreSQL-backed app
165
+
166
+ Without the flag the generated app has **no database**: it boots on
167
+ `MemoryUserSource` / `MemoryTenantSource` and the default in-memory team and
168
+ subscription stores, which is ideal for a first run and for CI and is gone on
169
+ the next restart.
170
+
171
+ `--prisma` (or `--db`) generates the database-backed shape instead:
172
+
173
+ | File | What it holds |
174
+ | --- | --- |
175
+ | `prisma/schema.prisma` | The reference models of every `@basaltkit/*-prisma` package the project uses — the same blocks `basalt prisma:sync` merges — plus a `Project` model of your own |
176
+ | `prisma.config.ts` | Prisma 7 keeps the connection URL here. It reads `MY_APP_DATABASE_URL` first and the bare `DATABASE_URL` only as a fallback — the same precedence as `src/env.ts`, so the CLI and the app can never mean two different databases |
177
+ | `src/db.ts` | `prisma` (unscoped — the framework stores use it, they run before a tenant is known) and, with tenancy, `db = prisma.$extends(tenancyExtension())` |
178
+ | `src/app.ts` | `prismaPlugin({ client: db, assertMigrated: true })`, `prismaTenantSource`, `prismaAuthStores`, `prismaTeamsStores`, `prismaSubscriptionsStores` |
179
+ | `prisma/seed.ts` | The `demo` tenant the header and subdomain resolvers expect |
180
+
181
+ ```bash
182
+ pnpm create basalt my-app --prisma
183
+ cd my-app && pnpm install # postinstall runs `prisma generate`
184
+ cp .env.example .env # point MY_APP_DATABASE_URL at your database
185
+ pnpm db:migrate # prisma migrate dev — creates the tables and seeds `demo`
186
+ pnpm dev
187
+ ```
188
+
189
+ **Migrations, never `prisma db push`.** The generated app boots with
190
+ `assertMigrated: true`, which refuses to start unless the database it reached has
191
+ the `_prisma_migrations` table — written by `prisma migrate dev` /
192
+ `prisma migrate deploy`, and *not* by `db push`. That is what turns a wrong
193
+ `DATABASE_URL` (a shell that exported another project's) into a boot error naming
194
+ the database and host instead of a `500` on the first request. In production:
195
+ `pnpm db:deploy`.
196
+
197
+ The generated `tests/app.test.ts` skips itself when no database is configured, so
198
+ `pnpm test` stays green on a machine without PostgreSQL. To add another Basalt
199
+ domain later, install its `@basaltkit/<domain>-prisma` package, run
200
+ `pnpm basalt prisma:sync` (with `--cli`) and then `pnpm db:migrate`.
201
+
162
202
  ### With `--cli`: the `basalt` command line
163
203
 
164
204
  ```bash
@@ -176,7 +216,28 @@ With pnpm 11, every `pnpm <script>` and `pnpm exec` first verifies dependencies
176
216
 
177
217
  ### Environment variables and `--env-file`
178
218
 
179
- Nothing loads `.env` for you. `node --env-file=.env` / `tsx --env-file=.env` **never override a variable already exported in the shell** — in a terminal where another project exported `DATABASE_URL` or `PORT`, the app boots against that value and only fails on the first request that touches it. The generated `.env.example` and README say so and suggest an app-specific prefix derived from the project name (`my-saas` → `MY_SAAS_DATABASE_URL`).
219
+ Nothing loads `.env` for you. `node --env-file=.env` / `tsx --env-file=.env` **never override a variable already exported in the shell** — in a terminal where another project exported `DATABASE_URL` or `PORT`, an app reading the generic name boots against that value and only fails on the first request that touches it.
220
+
221
+ The scaffold closes that trap instead of only warning about it: `src/env.ts` passes an **app-specific prefix** derived from the project name (`my-saas` → `MY_SAAS`) to `defineEnv`, and `.env.example` uses the prefixed names.
222
+
223
+ ```ts
224
+ // src/env.ts (generated)
225
+ export const env = defineEnv(
226
+ { PORT: z.coerce.number().default(3000), /* … */ },
227
+ { prefix: 'MY_SAAS' },
228
+ )
229
+ ```
230
+
231
+ ```bash
232
+ # .env.example (generated)
233
+ MY_SAAS_PORT=3000
234
+ MY_SAAS_HOST=0.0.0.0
235
+ MY_SAAS_LOG_LEVEL=info
236
+ NODE_ENV=development # never prefixed — a Node-wide convention
237
+ # MY_SAAS_APP_SECRET= # with auth
238
+ ```
239
+
240
+ Each variable is read as `MY_SAAS_<NAME>` first and **falls back** to the bare `<NAME>`, so a deployment that already exports the generic names keeps booting — while a stray `PORT` in your shell no longer wins. The rest of the app is unchanged: the keys stay bare (`env.PORT`). To require the prefixed names only, edit the generated file to `prefix: { value: 'MY_SAAS', fallback: false }`. Full rules: [`@basaltkit/env`](https://github.com/basaltkit/basalt/tree/main/packages/env#app-specific-prefix-prefix).
180
241
 
181
242
  ### Programmatic usage (Advanced)
182
243
 
@@ -219,6 +280,7 @@ Exported from `create-basalt` (in addition to the `create-basalt` executable):
219
280
  | `ui` | `boolean` | No | `false` | Generate the `web/` frontend |
220
281
  | `cli` | `boolean` | No | `false` | Generate the `basalt` CLI |
221
282
  | `mcp` | `boolean` | No | `false` | Expose read-only routes as MCP tools at `/mcp` |
283
+ | `prisma` | `boolean` | No | `false` | Back the app with PostgreSQL through Prisma (schema, migrations, Prisma-backed stores, `assertMigrated`) |
222
284
  | `resolveLatest` | `boolean` | No | `false` | Resolve every dependency to `^<latest>` from the npm registry before writing (falls back to the bundled ranges on any registry failure) |
223
285
  | `registry` | `ResolveLatestOptions` | No | — | `{ fetch?, registry?, timeoutMs?, overallTimeoutMs?, concurrency?, minimumReleaseAge?, now? }` — injectable fetch (tests), registry URL (default `npm_config_registry` or `https://registry.npmjs.org`), timeouts (5 s per request, 15 s overall), release-age window in minutes (default `pnpm_config_minimum_release_age` / `npm_config_minimum_release_age`, else 1440; `0` disables the probe), clock (tests) |
224
286
 
@@ -246,7 +308,7 @@ Error (extends `Error`) with the message `Target directory "<dir>" already exist
246
308
  | Type | Description |
247
309
  | --- | --- |
248
310
  | `PackageManager` | `'pnpm' \| 'npm' \| 'yarn' \| 'bun'` |
249
- | `ProjectOptions` | `{ name, tenancy, auth, billing, ui, cli }` — all resolved (no optionals) |
311
+ | `ProjectOptions` | `{ name, tenancy, auth, billing, ui, cli, mcp, prisma }` — all resolved (no optionals) |
250
312
  | `CreateProjectInput`, `CreateProjectResult` | Described above |
251
313
 
252
314
  ## Common errors and solutions (FAQ)
@@ -267,10 +329,24 @@ Outside an interactive terminal (no TTY) the questions are skipped. Always pass
267
329
  Automatic installation failed (network, Node version, etc.). Go into the folder and run `pnpm install` (or the indicated manager) to see the real error.
268
330
 
269
331
  **I started the app and `GET /auth/login` gives a secret error.**
270
- With auth on, `src/env.ts` requires `APP_SECRET` with at least 16 characters (there's a development default `change-me-in-production--`). Copy `.env.example` to `.env` and set your own secret before going to production.
332
+ With auth on, `src/env.ts` requires `APP_SECRET` — read as `MY_SAAS_APP_SECRET`, with at least 32 characters (`secret()` supplies a development default only when `NODE_ENV` is explicitly `development`/`test`). Copy `.env.example` to `.env` and set your own secret before going to production.
333
+
334
+ **With `--prisma`, the app refuses to boot: `PRISMA_NOT_MIGRATED`.**
335
+ `assertMigrated` reached a database without the `_prisma_migrations` table. The
336
+ message names the database and host it actually reached (never the
337
+ credentials): either it is not the database you meant — check
338
+ `env | grep DATABASE_URL`, a shell may have exported another project's — or it
339
+ was never migrated: run `pnpm db:migrate` (development) or `pnpm db:deploy`
340
+ (production). A database set up with `prisma db push` has no
341
+ `_prisma_migrations` at all; this scaffold is migration-based on purpose.
342
+
343
+ **With `--prisma`, `pnpm typecheck` can't find `./generated/prisma/client.js`.**
344
+ The Prisma client is generated code, not a checked-in file. `pnpm install` runs
345
+ `prisma generate` for you (`postinstall`); after a `git clone` without install,
346
+ or after editing the schema, run `pnpm db:generate`.
271
347
 
272
348
  **My app connects to the wrong database / port.**
273
- A variable exported in your shell beats `--env-file`. Check `env | grep DATABASE_URL`, start with `env -u DATABASE_URL pnpm dev`, and prefer app-prefixed names (see *Environment variables and `--env-file`*).
349
+ A variable exported in your shell beats `--env-file`. The generated `src/env.ts` already reads app-prefixed names (`MY_SAAS_PORT`), so set those rather than the generic ones — the bare names are only a fallback. Check `env | grep DATABASE_URL`, and see *Environment variables and `--env-file`*.
274
350
 
275
351
  **`pnpm basalt …` starts by running `pnpm install` (or fails offline).**
276
352
  pnpm 11's `verifyDepsBeforeRun` (default `install`). Run `node_modules/.bin/tsx bin/basalt.ts …` instead, or set `verifyDepsBeforeRun: warn` in `pnpm-workspace.yaml`.
@@ -286,6 +362,7 @@ There's no "re-scaffold" command. Either generate a new project with the right f
286
362
  - **`@basaltkit/tenancy`** — included by default (remove with `--no-tenancy`): header and subdomain resolvers, with a demo `MemoryTenantSource`.
287
363
  - **`@basaltkit/auth`** — included by default (remove with `--no-auth`): `/auth/*` routes and `APP_SECRET` validated in `env.ts`.
288
364
  - **`@basaltkit/subscriptions`** — with `--billing`: example `free`/`pro` plans with trial and feature limits.
365
+ - **`@basaltkit/prisma` + `@basaltkit/{tenancy,auth,teams,subscriptions}-prisma`** — with `--prisma`: the PostgreSQL layer — `prismaPlugin` (tenant-scoped client, boot-time `assertMigrated`), the composed `prisma/schema.prisma` and the Prisma-backed stores for whichever domains are on.
289
366
  - **`@basaltkit/cli` + `@basaltkit/generator`** — with `--cli`: `bin/basalt.ts` calls `runCli`, and `commandsPlugin(generatorCommands())` registers the `make:*` generators.
290
367
  - **`@basaltkit/sdk` + `@basaltkit/admin-shadcn` + `@basaltkit/admin`** — with `--ui`: the `web/` frontend calls the API through a typed client and uses the shadcn components.
291
368
  - **`@basaltkit/testing`** — always present in `devDependencies`, with a generated smoke test in `tests/app.test.ts`.
package/dist/args.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PackageManager } from './index.js';
2
- export declare const USAGE = "Usage: npm create basalt <name> [options]\n\nOptions:\n --dir=<path> Target directory (default: ./<name>)\n --no-tenancy Skip multi-tenancy\n --no-auth Skip authentication\n --billing Include subscriptions/billing\n --ui Scaffold a web/ frontend (React + shadcn + SDK)\n --cli Scaffold the 'basalt' CLI (code generators + commands)\n --mcp Expose read-only routes as MCP tools at /mcp\n --install Install dependencies (default: yes in an interactive\n terminal, skipped in CI/non-TTY; --no-install to opt out)\n --no-install Never install dependencies\n --git Initialize a git repository with a first commit\n (same default rule as --install; --no-git to opt out)\n --no-git Never initialize a git repository\n --offline Don't query the npm registry for the latest versions;\n use the ranges bundled with this create-basalt release\n --pm=<manager> Package manager: pnpm | npm | yarn | bun (default: auto-detect)\n -y, --yes Skip prompts and accept defaults\n -h, --help Show this help\n\nRun with no name in a terminal to be prompted interactively.\n";
2
+ export declare const USAGE = "Usage: npm create basalt <name> [options]\n\nOptions:\n --dir=<path> Target directory (default: ./<name>)\n --no-tenancy Skip multi-tenancy\n --no-auth Skip authentication\n --billing Include subscriptions/billing\n --ui Scaffold a web/ frontend (React + shadcn + SDK)\n --cli Scaffold the 'basalt' CLI (code generators + commands)\n --mcp Expose read-only routes as MCP tools at /mcp\n --prisma, --db Back the app with PostgreSQL through Prisma: schema,\n migrations, prisma-backed stores and a boot-time check\n that the database is the migrated one\n --install Install dependencies (default: yes in an interactive\n terminal, skipped in CI/non-TTY; --no-install to opt out)\n --no-install Never install dependencies\n --git Initialize a git repository with a first commit\n (same default rule as --install; --no-git to opt out)\n --no-git Never initialize a git repository\n --offline Don't query the npm registry for the latest versions;\n use the ranges bundled with this create-basalt release\n --pm=<manager> Package manager: pnpm | npm | yarn | bun (default: auto-detect)\n -y, --yes Skip prompts and accept defaults\n -h, --help Show this help\n\nRun with no name in a terminal to be prompted interactively.\n";
3
3
  export interface Flags {
4
4
  name?: string;
5
5
  dir?: string;
@@ -9,6 +9,7 @@ export interface Flags {
9
9
  ui: boolean;
10
10
  cli: boolean;
11
11
  mcp: boolean;
12
+ prisma: boolean;
12
13
  /** Tri-state: undefined = decide from the environment (TTY yes, CI no). */
13
14
  install?: boolean;
14
15
  git?: boolean;
package/dist/args.js CHANGED
@@ -9,6 +9,9 @@ Options:
9
9
  --ui Scaffold a web/ frontend (React + shadcn + SDK)
10
10
  --cli Scaffold the 'basalt' CLI (code generators + commands)
11
11
  --mcp Expose read-only routes as MCP tools at /mcp
12
+ --prisma, --db Back the app with PostgreSQL through Prisma: schema,
13
+ migrations, prisma-backed stores and a boot-time check
14
+ that the database is the migrated one
12
15
  --install Install dependencies (default: yes in an interactive
13
16
  terminal, skipped in CI/non-TTY; --no-install to opt out)
14
17
  --no-install Never install dependencies
@@ -31,6 +34,7 @@ export function parseArgs(argv) {
31
34
  ui: false,
32
35
  cli: false,
33
36
  mcp: false,
37
+ prisma: false,
34
38
  yes: false,
35
39
  offline: false,
36
40
  };
@@ -47,6 +51,10 @@ export function parseArgs(argv) {
47
51
  flags.cli = true;
48
52
  else if (token === '--mcp')
49
53
  flags.mcp = true;
54
+ // `--db` reads naturally in a sentence ("scaffold it with a db") and is the
55
+ // same switch; the canonical spelling stays the package name, like --mcp.
56
+ else if (token === '--prisma' || token === '--db')
57
+ flags.prisma = true;
50
58
  else if (token === '--install')
51
59
  flags.install = true;
52
60
  else if (token === '--no-install')
package/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import { spawn } from 'node:child_process';
3
3
  import { stdin, stdout } from 'node:process';
4
4
  import { createProject, describeResolution, detectPackageManager, resolveRunDefaults, runWizard, ttyPrompter, TargetNotEmptyError, WizardCancelledError, } from './index.js';
5
5
  import { parseArgs, resolvesLatest, USAGE } from './args.js';
6
+ import { envPrefix } from './templates.js';
6
7
  /** Runs a command inheriting stdio; resolves false on non-zero exit (never throws). */
7
8
  function run(command, args, cwd) {
8
9
  return new Promise((resolve) => {
@@ -35,6 +36,7 @@ if (!flags.yes && flags.name === undefined && stdin.isTTY) {
35
36
  flags.ui = result.ui;
36
37
  flags.cli = result.cli;
37
38
  flags.mcp = result.mcp;
39
+ flags.prisma = result.prisma;
38
40
  flags.install = result.install;
39
41
  flags.git = result.git;
40
42
  flags.pm = result.pm;
@@ -68,6 +70,7 @@ try {
68
70
  ui: flags.ui,
69
71
  cli: flags.cli,
70
72
  mcp: flags.mcp,
73
+ prisma: flags.prisma,
71
74
  // New apps get the latest published version of every dependency (with the
72
75
  // bundled ranges as an offline fallback); --offline skips the registry.
73
76
  resolveLatest: resolvesLatest(flags),
@@ -108,6 +111,12 @@ try {
108
111
  const steps = [`cd ${result.dir}`];
109
112
  if (!run_.install)
110
113
  steps.push(`${pm} install`);
114
+ if (flags.prisma) {
115
+ // Nothing boots before the database exists: env.DATABASE_URL is required and
116
+ // the app asserts at boot that it reached the MIGRATED database.
117
+ steps.push(`cp .env.example .env # then point ${envPrefix(result.options.name)}_DATABASE_URL at your database`);
118
+ steps.push(`${pm} run db:migrate${flags.tenancy ? ' # creates the tables and seeds the demo tenant' : ' # creates the tables'}`);
119
+ }
111
120
  steps.push(`${pm} run dev${flags.ui ? ' # API on :3000' : ''}`);
112
121
  // `web` is wired as a pnpm workspace member (pnpm-workspace.yaml), so its dev
113
122
  // server is launched with a pnpm filter regardless of the root package manager.
package/dist/index.d.ts CHANGED
@@ -14,6 +14,12 @@ export interface CreateProjectInput {
14
14
  cli?: boolean;
15
15
  /** Expose opted-in routes as MCP tools over HTTP at `/mcp`. Default: false. */
16
16
  mcp?: boolean;
17
+ /**
18
+ * Back the app with PostgreSQL through Prisma: `prisma/schema.prisma`,
19
+ * `src/db.ts`, `prismaPlugin({ assertMigrated: true })` and the Prisma-backed
20
+ * stores instead of the in-memory ones. Default: false (no database at all).
21
+ */
22
+ prisma?: boolean;
17
23
  /**
18
24
  * Resolve every dependency to its latest published version (npm registry)
19
25
  * before writing files. Default: false — the embedded fallback ranges are
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { mkdir, readdir, writeFile } from 'node:fs/promises';
2
2
  import { dirname, join, resolve } from 'node:path';
3
- import { appTest, appTs, devTs, dockerignore, envExample, envTs, gitignore, mcpJson, basaltBin, packageJson, pnpmWorkspaceYaml, readme, routesTs, serverTs, tsconfigJson, } from './templates.js';
3
+ import { appTest, appTs, dbTs, devTs, dockerignore, envExample, envTs, gitignore, mcpJson, basaltBin, packageJson, pnpmWorkspaceYaml, prismaConfigTs, prismaSchema, prismaSeedTs, readme, routesTs, serverTs, tsconfigJson, } from './templates.js';
4
4
  import { uiFiles } from './templates-ui.js';
5
5
  import { applyVersions, collectDependencies, resolveLatestVersions, } from './latest-versions.js';
6
6
  export class TargetNotEmptyError extends Error {
@@ -45,6 +45,7 @@ export async function createProject(input) {
45
45
  ui: input.ui ?? false,
46
46
  cli: input.cli ?? false,
47
47
  mcp: input.mcp ?? false,
48
+ prisma: input.prisma ?? false,
48
49
  };
49
50
  const dir = resolve(input.dir ?? input.name);
50
51
  const existing = await readdir(dir).catch(() => null);
@@ -52,9 +53,9 @@ export async function createProject(input) {
52
53
  throw new TargetNotEmptyError(dir);
53
54
  const files = {
54
55
  'package.json': packageJson(options),
55
- 'tsconfig.json': tsconfigJson(),
56
+ 'tsconfig.json': tsconfigJson(options),
56
57
  '.env.example': envExample(options),
57
- '.gitignore': gitignore(),
58
+ '.gitignore': gitignore(options),
58
59
  '.dockerignore': dockerignore(),
59
60
  'README.md': readme(options),
60
61
  'pnpm-workspace.yaml': pnpmWorkspaceYaml(options),
@@ -64,6 +65,15 @@ export async function createProject(input) {
64
65
  'src/server.ts': serverTs(),
65
66
  'src/dev.ts': devTs(),
66
67
  'tests/app.test.ts': appTest(options),
68
+ ...(options.prisma
69
+ ? {
70
+ 'prisma/schema.prisma': prismaSchema(options),
71
+ 'prisma.config.ts': prismaConfigTs(options),
72
+ 'src/db.ts': dbTs(options),
73
+ // The demo tenant only exists where tenancy does.
74
+ ...(options.tenancy ? { 'prisma/seed.ts': prismaSeedTs(options) } : {}),
75
+ }
76
+ : {}),
67
77
  ...(options.cli ? { 'bin/basalt.ts': basaltBin() } : {}),
68
78
  ...(options.mcp ? { '.mcp.json': mcpJson(options) } : {}),
69
79
  ...(options.ui ? uiFiles(options) : {}),
@@ -31,12 +31,17 @@
31
31
  * major. Bump a range to a new major only together with the templates.
32
32
  */
33
33
  export const THIRD_PARTY_VERSIONS = {
34
+ '@prisma/adapter-pg': '^7.10.0',
35
+ '@prisma/client': '^7.10.0',
34
36
  '@tailwindcss/vite': '^4.3.3',
35
37
  '@types/node': '^26.6.2',
38
+ '@types/pg': '^8.15.6',
36
39
  '@types/react': '^19.3.0',
37
40
  '@types/react-dom': '^19.3.0',
38
41
  '@vitejs/plugin-react': '^6.1.1',
42
+ pg: '^8.16.3',
39
43
  'pino-pretty': '^13.1.3',
44
+ prisma: '^7.10.0',
40
45
  react: '^19.3.0',
41
46
  'react-dom': '^19.3.0',
42
47
  tailwindcss: '^4.3.3',
@@ -9,6 +9,12 @@ export interface ProjectOptions {
9
9
  cli: boolean;
10
10
  /** Expose opted-in routes as MCP tools (@basaltkit/mcp) over HTTP at `/mcp`. */
11
11
  mcp: boolean;
12
+ /**
13
+ * Back the app with a real PostgreSQL database: a `prisma/schema.prisma`,
14
+ * `src/db.ts`, `prismaPlugin({ assertMigrated: true })` and the Prisma-backed
15
+ * stores instead of the in-memory ones.
16
+ */
17
+ prisma: boolean;
12
18
  }
13
19
  export { SCAFFOLD_VERSIONS } from './versions.js';
14
20
  export { THIRD_PARTY_VERSIONS } from './latest-versions.js';
@@ -29,7 +35,7 @@ export declare const thirdPartyVersionOf: (pkg: string) => string;
29
35
  /** True when the scaffold binds users to the tenant they act on (tenancy + auth). */
30
36
  export declare const enforcesMembership: (options: ProjectOptions) => boolean;
31
37
  export declare function packageJson(options: ProjectOptions): string;
32
- export declare function tsconfigJson(): string;
38
+ export declare function tsconfigJson(options: ProjectOptions): string;
33
39
  export declare function envTs(options: ProjectOptions): string;
34
40
  /**
35
41
  * An app-specific env-var prefix derived from the project name
@@ -38,6 +44,8 @@ export declare function envTs(options: ProjectOptions): string;
38
44
  */
39
45
  export declare function envPrefix(name: string): string;
40
46
  export declare function envExample(options: ProjectOptions): string;
47
+ /** A PostgreSQL-safe database name derived from the project name. */
48
+ export declare function databaseName(name: string): string;
41
49
  export declare function appTs(options: ProjectOptions): string;
42
50
  export declare function routesTs(options: ProjectOptions): string;
43
51
  export declare function serverTs(): string;
@@ -49,6 +57,34 @@ export declare function serverTs(): string;
49
57
  */
50
58
  export declare function devTs(): string;
51
59
  export declare function basaltBin(): string;
60
+ /**
61
+ * `prisma/schema.prisma` — the datasource, the client generator, the models of
62
+ * every Basalt domain this app enables, and the app's own first model.
63
+ *
64
+ * Prisma 7 no longer takes the connection URL here (prisma.config.ts does), and
65
+ * the client needs an explicit `output`; src/db.ts imports it from there.
66
+ */
67
+ export declare function prismaSchema(options: ProjectOptions): string;
68
+ /**
69
+ * `prisma.config.ts` — Prisma 7 reads the connection URL (and the seed command)
70
+ * from here instead of from schema.prisma. It applies the SAME precedence as
71
+ * src/env.ts: the app-prefixed name first, the bare `DATABASE_URL` only as a
72
+ * fallback, so the CLI and the running app can never mean different databases.
73
+ */
74
+ export declare function prismaConfigTs(options: ProjectOptions): string;
75
+ /**
76
+ * `src/db.ts` — the Prisma client(s). Two bindings on purpose: the unscoped
77
+ * `prisma` for the framework stores (they key their own rows and run outside a
78
+ * tenant context) and, with tenancy, the `db` client the whole application uses,
79
+ * where every query is filtered by the request's tenant.
80
+ */
81
+ export declare function dbTs(options: ProjectOptions): string;
82
+ /**
83
+ * `prisma/seed.ts` — creates the `demo` tenant the scaffolded resolvers expect
84
+ * (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed` and after
85
+ * `prisma migrate dev` (prisma.config.ts declares it).
86
+ */
87
+ export declare function prismaSeedTs(options: ProjectOptions): string;
52
88
  export declare function appTest(options: ProjectOptions): string;
53
89
  export declare function readme(options: ProjectOptions): string;
54
90
  /**
@@ -65,7 +101,7 @@ export declare function mcpJson(_options: ProjectOptions): string;
65
101
  * `certs/server.key` through.
66
102
  */
67
103
  export declare function dockerignore(): string;
68
- export declare function gitignore(): string;
104
+ export declare function gitignore(options: ProjectOptions): string;
69
105
  /**
70
106
  * pnpm settings: esbuild's build script is required by tsx;
71
107
  * msgpackr-extract (optional native accelerator via BullMQ) is declined —