create-basalt 1.7.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 +59 -3
- package/dist/args.d.ts +2 -1
- package/dist/args.js +8 -0
- package/dist/cli.js +9 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +13 -3
- package/dist/latest-versions.js +5 -0
- package/dist/templates.d.ts +38 -2
- package/dist/templates.js +541 -24
- package/dist/versions.js +6 -2
- package/dist/wizard.d.ts +2 -1
- package/dist/wizard.js +4 -2
- package/package.json +15 -6
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
|
|
@@ -147,7 +149,7 @@ my-app/
|
|
|
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
|
|
@@ -240,6 +280,7 @@ Exported from `create-basalt` (in addition to the `create-basalt` executable):
|
|
|
240
280
|
| `ui` | `boolean` | No | `false` | Generate the `web/` frontend |
|
|
241
281
|
| `cli` | `boolean` | No | `false` | Generate the `basalt` CLI |
|
|
242
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`) |
|
|
243
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) |
|
|
244
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) |
|
|
245
286
|
|
|
@@ -267,7 +308,7 @@ Error (extends `Error`) with the message `Target directory "<dir>" already exist
|
|
|
267
308
|
| Type | Description |
|
|
268
309
|
| --- | --- |
|
|
269
310
|
| `PackageManager` | `'pnpm' \| 'npm' \| 'yarn' \| 'bun'` |
|
|
270
|
-
| `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) |
|
|
271
312
|
| `CreateProjectInput`, `CreateProjectResult` | Described above |
|
|
272
313
|
|
|
273
314
|
## Common errors and solutions (FAQ)
|
|
@@ -290,6 +331,20 @@ Automatic installation failed (network, Node version, etc.). Go into the folder
|
|
|
290
331
|
**I started the app and `GET /auth/login` gives a secret error.**
|
|
291
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.
|
|
292
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`.
|
|
347
|
+
|
|
293
348
|
**My app connects to the wrong database / port.**
|
|
294
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`*.
|
|
295
350
|
|
|
@@ -307,6 +362,7 @@ There's no "re-scaffold" command. Either generate a new project with the right f
|
|
|
307
362
|
- **`@basaltkit/tenancy`** — included by default (remove with `--no-tenancy`): header and subdomain resolvers, with a demo `MemoryTenantSource`.
|
|
308
363
|
- **`@basaltkit/auth`** — included by default (remove with `--no-auth`): `/auth/*` routes and `APP_SECRET` validated in `env.ts`.
|
|
309
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.
|
|
310
366
|
- **`@basaltkit/cli` + `@basaltkit/generator`** — with `--cli`: `bin/basalt.ts` calls `runCli`, and `commandsPlugin(generatorCommands())` registers the `make:*` generators.
|
|
311
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.
|
|
312
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) : {}),
|
package/dist/latest-versions.js
CHANGED
|
@@ -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',
|
package/dist/templates.d.ts
CHANGED
|
@@ -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 —
|
package/dist/templates.js
CHANGED
|
@@ -46,13 +46,34 @@ export function packageJson(options) {
|
|
|
46
46
|
basalt.push('@basaltkit/mcp');
|
|
47
47
|
if (options.cli) {
|
|
48
48
|
// Runtime deps: app.ts uses commandsPlugin (@basaltkit/cli); prisma powers
|
|
49
|
-
// `basalt prisma:sync` (and prismaPlugin
|
|
50
|
-
//
|
|
49
|
+
// `basalt prisma:sync` (and prismaPlugin, wired by --prisma).
|
|
50
|
+
// @basaltkit/generator is dev-only (below).
|
|
51
51
|
basalt.push('@basaltkit/cli', '@basaltkit/prisma');
|
|
52
52
|
}
|
|
53
|
+
if (options.prisma) {
|
|
54
|
+
// The database layer: prismaPlugin + tenancyExtension, plus the store
|
|
55
|
+
// packages for whichever domains are on (the memory ones are gone).
|
|
56
|
+
if (!options.cli)
|
|
57
|
+
basalt.push('@basaltkit/prisma');
|
|
58
|
+
if (options.tenancy)
|
|
59
|
+
basalt.push('@basaltkit/tenancy-prisma');
|
|
60
|
+
if (options.auth)
|
|
61
|
+
basalt.push('@basaltkit/auth-prisma');
|
|
62
|
+
if (enforcesMembership(options))
|
|
63
|
+
basalt.push('@basaltkit/teams-prisma');
|
|
64
|
+
if (options.billing)
|
|
65
|
+
basalt.push('@basaltkit/subscriptions-prisma');
|
|
66
|
+
}
|
|
53
67
|
const dependencies = { zod: thirdPartyVersionOf('zod') };
|
|
54
68
|
for (const pkg of basalt)
|
|
55
69
|
dependencies[pkg] = versionOf(pkg);
|
|
70
|
+
if (options.prisma) {
|
|
71
|
+
// Prisma 7 talks to PostgreSQL through a driver adapter, so `pg` is a real
|
|
72
|
+
// runtime dependency (see src/db.ts); the `prisma` CLI is dev-only.
|
|
73
|
+
for (const pkg of ['@prisma/adapter-pg', '@prisma/client', 'pg']) {
|
|
74
|
+
dependencies[pkg] = thirdPartyVersionOf(pkg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
56
77
|
const devDependencies = {
|
|
57
78
|
'@basaltkit/testing': versionOf('@basaltkit/testing'),
|
|
58
79
|
};
|
|
@@ -70,6 +91,10 @@ export function packageJson(options) {
|
|
|
70
91
|
// app ships and runs without it (see `.mcp.json` for the client config).
|
|
71
92
|
devDependencies['@basaltkit/ai-mcp'] = versionOf('@basaltkit/ai-mcp');
|
|
72
93
|
}
|
|
94
|
+
if (options.prisma) {
|
|
95
|
+
for (const pkg of ['@types/pg', 'prisma'])
|
|
96
|
+
devDependencies[pkg] = thirdPartyVersionOf(pkg);
|
|
97
|
+
}
|
|
73
98
|
return `${JSON.stringify({
|
|
74
99
|
name: options.name,
|
|
75
100
|
version: '0.1.0',
|
|
@@ -83,12 +108,27 @@ export function packageJson(options) {
|
|
|
83
108
|
test: 'vitest run',
|
|
84
109
|
typecheck: 'tsc --noEmit',
|
|
85
110
|
...(options.cli ? { basalt: 'tsx bin/basalt.ts' } : {}),
|
|
111
|
+
...(options.prisma
|
|
112
|
+
? {
|
|
113
|
+
// Generate right after install so `pnpm typecheck` has the client
|
|
114
|
+
// types (generation needs no database).
|
|
115
|
+
postinstall: 'prisma generate',
|
|
116
|
+
'db:generate': 'prisma generate',
|
|
117
|
+
// Development and deployment BOTH go through migrations: they are
|
|
118
|
+
// what creates `_prisma_migrations`, which the app's
|
|
119
|
+
// prismaPlugin({ assertMigrated: true }) requires at boot. There
|
|
120
|
+
// is deliberately no `db:push` script.
|
|
121
|
+
'db:migrate': 'prisma migrate dev',
|
|
122
|
+
'db:deploy': 'prisma migrate deploy',
|
|
123
|
+
...(options.tenancy ? { 'db:seed': 'tsx prisma/seed.ts' } : {}),
|
|
124
|
+
}
|
|
125
|
+
: {}),
|
|
86
126
|
},
|
|
87
127
|
dependencies: Object.fromEntries(Object.entries(dependencies).sort()),
|
|
88
128
|
devDependencies: Object.fromEntries(Object.entries(devDependencies).sort()),
|
|
89
129
|
}, null, 2)}\n`;
|
|
90
130
|
}
|
|
91
|
-
export function tsconfigJson() {
|
|
131
|
+
export function tsconfigJson(options) {
|
|
92
132
|
return `${JSON.stringify({
|
|
93
133
|
compilerOptions: {
|
|
94
134
|
target: 'ES2022',
|
|
@@ -101,7 +141,10 @@ export function tsconfigJson() {
|
|
|
101
141
|
noEmit: true,
|
|
102
142
|
types: ['node'],
|
|
103
143
|
},
|
|
104
|
-
|
|
144
|
+
// The generated Prisma client lives under src/generated (git-ignored, and
|
|
145
|
+
// re-created by `prisma generate`); the seed script under prisma/ is
|
|
146
|
+
// type-checked too.
|
|
147
|
+
include: ['src', 'tests', ...(options.prisma && options.tenancy ? ['prisma/seed.ts'] : [])],
|
|
105
148
|
}, null, 2)}\n`;
|
|
106
149
|
}
|
|
107
150
|
export function envTs(options) {
|
|
@@ -119,7 +162,14 @@ export const env = defineEnv(
|
|
|
119
162
|
LOG_LEVEL: z.enum(LOG_LEVELS).default('info'),
|
|
120
163
|
// Unset counts as production (fail-closed); \`pnpm dev\` sets development.
|
|
121
164
|
// NODE_ENV is a Node-wide convention and is never prefixed.
|
|
122
|
-
NODE_ENV: z.enum(['development', 'production', 'test']).default('production'),${options.
|
|
165
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('production'),${options.prisma
|
|
166
|
+
? `
|
|
167
|
+
// Required: the PostgreSQL connection string, read as ${prefix}_DATABASE_URL
|
|
168
|
+
// first (a stray DATABASE_URL exported in your shell must not win — that is
|
|
169
|
+
// how an app boots against another project's database). src/db.ts builds the
|
|
170
|
+
// Prisma client from it; prisma.config.ts gives the CLI the same URL.
|
|
171
|
+
DATABASE_URL: z.string().min(1),`
|
|
172
|
+
: ''}${options.auth
|
|
123
173
|
? `
|
|
124
174
|
// Signs JWTs and sessions. secret() is fail-closed: required unless NODE_ENV
|
|
125
175
|
// is explicitly development/test (no fallback when NODE_ENV is unset),
|
|
@@ -179,10 +229,20 @@ ${options.auth
|
|
|
179
229
|
# without it. Generate a strong one: openssl rand -base64 48
|
|
180
230
|
# ${prefix}_APP_SECRET=
|
|
181
231
|
`
|
|
182
|
-
: ''}
|
|
183
|
-
|
|
232
|
+
: ''}${options.prisma
|
|
233
|
+
? `# Required: the app does not boot without it. The Prisma CLI reads the same
|
|
234
|
+
# name from prisma.config.ts, so \`pnpm db:migrate\` and the running app always
|
|
235
|
+
# agree on which database they mean.
|
|
236
|
+
${prefix}_DATABASE_URL=postgres://postgres:postgres@localhost:5432/${databaseName(options.name)}
|
|
237
|
+
`
|
|
238
|
+
: `# When you add a database, declare DATABASE_URL in src/env.ts and set it here
|
|
239
|
+
# under the prefixed name (or scaffold with --prisma, which does it for you):
|
|
184
240
|
# ${prefix}_DATABASE_URL=postgres://user:pass@localhost:5432/${options.name.replace(/^@[^/]+\//, '')}
|
|
185
|
-
`;
|
|
241
|
+
`}`;
|
|
242
|
+
}
|
|
243
|
+
/** A PostgreSQL-safe database name derived from the project name. */
|
|
244
|
+
export function databaseName(name) {
|
|
245
|
+
return name.replace(/^@[^/]+\//, '').replace(/[^a-zA-Z0-9]+/g, '_').toLowerCase();
|
|
186
246
|
}
|
|
187
247
|
export function appTs(options) {
|
|
188
248
|
const imports = [
|
|
@@ -207,24 +267,73 @@ export function appTs(options) {
|
|
|
207
267
|
})`,
|
|
208
268
|
];
|
|
209
269
|
let routesExpression = 'appRoutes';
|
|
270
|
+
/** Module-level bindings emitted between the imports and `buildApp`. */
|
|
271
|
+
const preamble = [];
|
|
272
|
+
if (options.prisma) {
|
|
273
|
+
imports.push(`import { prismaPlugin } from '@basaltkit/prisma'`);
|
|
274
|
+
imports.push(`import { db, prisma } from './db.js'`);
|
|
275
|
+
plugins.push(`prismaPlugin({
|
|
276
|
+
// \`db\` is the tenant-scoped client (src/db.ts): db<PrismaClient>() in a
|
|
277
|
+
// handler returns it, already filtered by the request's tenant.
|
|
278
|
+
client: db,
|
|
279
|
+
// Fail the BOOT when this database is not the migrated one — a shell that
|
|
280
|
+
// exported another project's DATABASE_URL, a typo in the database name.
|
|
281
|
+
// Without it the app starts and dies on the first query instead. Needs
|
|
282
|
+
// \`prisma migrate dev\`/\`migrate deploy\` (they write _prisma_migrations);
|
|
283
|
+
// \`prisma db push\` does not, so never use it against this app.
|
|
284
|
+
assertMigrated: true,
|
|
285
|
+
})`);
|
|
286
|
+
}
|
|
210
287
|
if (options.tenancy) {
|
|
211
|
-
|
|
212
|
-
|
|
288
|
+
if (options.prisma) {
|
|
289
|
+
imports.push(`import { headerResolver, subdomainResolver, tenancyPlugin } from '@basaltkit/tenancy'`);
|
|
290
|
+
imports.push(`import { prismaTenantSource } from '@basaltkit/tenancy-prisma'`);
|
|
291
|
+
// The stores read their own tables, outside any tenant context (a login
|
|
292
|
+
// happens before a tenant is known) — so they take the UNSCOPED client.
|
|
293
|
+
preamble.push(`/** Tenants live in the \`tenants\` table — seed one with \`pnpm db:seed\`. */
|
|
294
|
+
const tenants = prismaTenantSource(prisma)`);
|
|
295
|
+
plugins.push(`tenancyPlugin({
|
|
296
|
+
source: tenants,
|
|
297
|
+
resolvers: [headerResolver(), subdomainResolver({ base: 'localhost' })],
|
|
298
|
+
})`);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
imports.push(`import { headerResolver, MemoryTenantSource, subdomainResolver, tenancyPlugin } from '@basaltkit/tenancy'`);
|
|
302
|
+
plugins.push(`tenancyPlugin({
|
|
213
303
|
// Replace MemoryTenantSource with your database-backed source.
|
|
214
304
|
source: new MemoryTenantSource().add({ id: 'demo', name: 'Demo Tenant' }),
|
|
215
305
|
resolvers: [headerResolver(), subdomainResolver({ base: 'localhost' })],
|
|
216
306
|
})`);
|
|
307
|
+
}
|
|
217
308
|
}
|
|
218
309
|
if (options.auth) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
310
|
+
if (options.prisma) {
|
|
311
|
+
imports.push(`import { authPlugin, authRoutes, mfaRoutes } from '@basaltkit/auth'`);
|
|
312
|
+
imports.push(`import { prismaAuthStores } from '@basaltkit/auth-prisma'`);
|
|
313
|
+
imports.push(`import { env } from './env.js'`);
|
|
314
|
+
preamble.push(`/** Users, sessions, refresh tokens, email/reset tokens, MFA and API keys. */
|
|
315
|
+
const authStores = prismaAuthStores(prisma)`);
|
|
316
|
+
plugins.push(`authPlugin({
|
|
317
|
+
users: authStores.users,
|
|
318
|
+
sessions: authStores.sessions,
|
|
319
|
+
refreshTokens: authStores.refreshTokens,
|
|
320
|
+
// Email verification and password reset.
|
|
321
|
+
tokens: authStores.tokens,
|
|
322
|
+
mfa: authStores.mfa,
|
|
323
|
+
secret: env.APP_SECRET,
|
|
324
|
+
})`);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
imports.push(`import { authPlugin, authRoutes, mfaRoutes, MemoryUserSource } from '@basaltkit/auth'`);
|
|
328
|
+
imports.push(`import { env } from './env.js'`);
|
|
329
|
+
plugins.push(`authPlugin({
|
|
222
330
|
// Replace MemoryUserSource with your database-backed source (e.g.
|
|
223
331
|
// @basaltkit/auth-sqlite or @basaltkit/auth-prisma). The default in-memory
|
|
224
332
|
// MFA store is enough for the ready-made TOTP flow below.
|
|
225
333
|
users: new MemoryUserSource(),
|
|
226
334
|
secret: env.APP_SECRET,
|
|
227
335
|
})`);
|
|
336
|
+
}
|
|
228
337
|
// authRoutes(): register, login, logout, refresh, me, email verification and
|
|
229
338
|
// password recovery. mfaRoutes(): TOTP enroll/activate/status/disable.
|
|
230
339
|
routesExpression = '[...appRoutes, ...authRoutes(), ...mfaRoutes()]';
|
|
@@ -237,10 +346,21 @@ export function appTs(options) {
|
|
|
237
346
|
// meta: { central: true }.
|
|
238
347
|
imports[0] = `import { createApp, definePlugin } from '@basaltkit/core'`;
|
|
239
348
|
imports.push(`import { TEAMS, teamsPlugin, tenantMembershipPlugin } from '@basaltkit/teams'`);
|
|
240
|
-
|
|
349
|
+
if (options.prisma) {
|
|
350
|
+
imports.push(`import { prismaTeamsStores } from '@basaltkit/teams-prisma'`);
|
|
351
|
+
preamble.push(`/** Memberships and invitations (\`team_memberships\`, \`team_invitations\`). */
|
|
352
|
+
const teamStores = prismaTeamsStores(prisma)`);
|
|
353
|
+
plugins.push(`teamsPlugin({
|
|
354
|
+
memberships: teamStores.memberships,
|
|
355
|
+
invitations: teamStores.invitations,
|
|
356
|
+
})`);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
plugins.push(`teamsPlugin({
|
|
241
360
|
// TODO: the default membership/invitation stores are in-memory — pass
|
|
242
361
|
// persistent ones (memberships, invitations) before production.
|
|
243
362
|
})`);
|
|
363
|
+
}
|
|
244
364
|
plugins.push(`// Rejects authenticated requests for a tenant the user is not a member of.
|
|
245
365
|
tenantMembershipPlugin()`);
|
|
246
366
|
plugins.push(`// Dev-only seed: each new registrant joins the 'demo' tenant so the scaffold
|
|
@@ -250,12 +370,22 @@ export function appTs(options) {
|
|
|
250
370
|
}
|
|
251
371
|
if (options.billing) {
|
|
252
372
|
imports.push(`import { definePlans, subscriptionsPlugin } from '@basaltkit/subscriptions'`);
|
|
373
|
+
if (options.prisma) {
|
|
374
|
+
imports.push(`import { prismaSubscriptionsStores } from '@basaltkit/subscriptions-prisma'`);
|
|
375
|
+
preamble.push(`/** Subscriptions, usage counters and webhook idempotency. */
|
|
376
|
+
const subscriptionStores = prismaSubscriptionsStores(prisma)`);
|
|
377
|
+
}
|
|
253
378
|
plugins.push(`subscriptionsPlugin({
|
|
254
379
|
plans: definePlans({
|
|
255
380
|
free: { price: 0, features: { projects: 3 } },
|
|
256
381
|
pro: { price: { monthly: 29, yearly: 290 }, trial: '14d', features: { projects: 50 } },
|
|
257
382
|
}),
|
|
258
|
-
fallbackPlan: 'free'
|
|
383
|
+
fallbackPlan: 'free',${options.prisma
|
|
384
|
+
? `
|
|
385
|
+
store: subscriptionStores.store,
|
|
386
|
+
usage: subscriptionStores.usage,
|
|
387
|
+
webhooks: subscriptionStores.webhooks,`
|
|
388
|
+
: ''}
|
|
259
389
|
})`);
|
|
260
390
|
}
|
|
261
391
|
if (options.cli) {
|
|
@@ -284,7 +414,7 @@ export function appTs(options) {
|
|
|
284
414
|
: '';
|
|
285
415
|
return `${imports.join('\n')}
|
|
286
416
|
import { appRoutes } from './routes.js'
|
|
287
|
-
|
|
417
|
+
${preamble.length > 0 ? `\n${preamble.join('\n\n')}\n` : ''}
|
|
288
418
|
export interface BuildAppOptions {
|
|
289
419
|
logLevel?: LogLevel
|
|
290
420
|
pretty?: boolean${commandsField}
|
|
@@ -423,14 +553,338 @@ const app = buildApp({
|
|
|
423
553
|
process.exit(await runCli({ app }))
|
|
424
554
|
`;
|
|
425
555
|
}
|
|
556
|
+
/**
|
|
557
|
+
* The models each `@basaltkit/<domain>-prisma` package needs, copied VERBATIM
|
|
558
|
+
* from that package's reference `prisma/schema.prisma` — the same blocks
|
|
559
|
+
* `basalt prisma:sync` merges into an existing schema. The scaffold ships the
|
|
560
|
+
* merged result so a new app can run `prisma migrate dev` straight away;
|
|
561
|
+
* `tests/prisma.test.ts` fails if a package's reference schema drifts from the
|
|
562
|
+
* copy here.
|
|
563
|
+
*/
|
|
564
|
+
const PRISMA_MODELS = {
|
|
565
|
+
tenancy: `model Tenant {
|
|
566
|
+
id String @id
|
|
567
|
+
// The open tenant record ({ id, ...anything }) as JSON — attach whatever
|
|
568
|
+
// per-tenant fields you like; they round-trip unchanged.
|
|
569
|
+
data Json
|
|
570
|
+
domains TenantDomain[]
|
|
571
|
+
|
|
572
|
+
@@map("tenants")
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
model TenantDomain {
|
|
576
|
+
domain String @id
|
|
577
|
+
tenantId String
|
|
578
|
+
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
|
579
|
+
|
|
580
|
+
@@index([tenantId])
|
|
581
|
+
@@map("tenant_domains")
|
|
582
|
+
}`,
|
|
583
|
+
auth: `model AuthUser {
|
|
584
|
+
id String @id
|
|
585
|
+
email String @unique
|
|
586
|
+
passwordHash String
|
|
587
|
+
emailVerified Boolean @default(false)
|
|
588
|
+
|
|
589
|
+
@@map("auth_users")
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
model AuthSession {
|
|
593
|
+
id String @id
|
|
594
|
+
userId String
|
|
595
|
+
expiresAt DateTime
|
|
596
|
+
|
|
597
|
+
@@index([userId])
|
|
598
|
+
@@map("auth_sessions")
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
model AuthRefreshToken {
|
|
602
|
+
token String @id
|
|
603
|
+
familyId String
|
|
604
|
+
userId String
|
|
605
|
+
expiresAt DateTime
|
|
606
|
+
usedAt DateTime?
|
|
607
|
+
|
|
608
|
+
@@index([familyId])
|
|
609
|
+
@@index([userId])
|
|
610
|
+
@@map("auth_refresh_tokens")
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
model AuthToken {
|
|
614
|
+
token String @id
|
|
615
|
+
userId String
|
|
616
|
+
purpose String
|
|
617
|
+
expiresAt DateTime
|
|
618
|
+
usedAt DateTime?
|
|
619
|
+
|
|
620
|
+
@@index([userId, purpose])
|
|
621
|
+
@@map("auth_tokens")
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
model AuthApiKey {
|
|
625
|
+
id String @id
|
|
626
|
+
name String
|
|
627
|
+
prefix String
|
|
628
|
+
hash String @unique
|
|
629
|
+
tenantId String?
|
|
630
|
+
userId String?
|
|
631
|
+
scopes String[]
|
|
632
|
+
createdAt DateTime
|
|
633
|
+
expiresAt DateTime?
|
|
634
|
+
lastUsedAt DateTime?
|
|
635
|
+
revokedAt DateTime?
|
|
636
|
+
|
|
637
|
+
@@index([tenantId])
|
|
638
|
+
@@index([userId])
|
|
639
|
+
@@map("auth_api_keys")
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
model AuthMfa {
|
|
643
|
+
userId String @id
|
|
644
|
+
secret String
|
|
645
|
+
enabled Boolean @default(false)
|
|
646
|
+
recoveryCodes String[]
|
|
647
|
+
lastUsedStep Int?
|
|
648
|
+
|
|
649
|
+
@@map("auth_mfa")
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
model AuthTokenVersion {
|
|
653
|
+
userId String @id
|
|
654
|
+
version Int @default(0)
|
|
655
|
+
|
|
656
|
+
@@map("auth_token_versions")
|
|
657
|
+
}`,
|
|
658
|
+
teams: `model TeamMembership {
|
|
659
|
+
tenantId String
|
|
660
|
+
userId String
|
|
661
|
+
role String
|
|
662
|
+
createdAt DateTime
|
|
663
|
+
|
|
664
|
+
@@id([tenantId, userId])
|
|
665
|
+
@@map("team_memberships")
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
model TeamInvitation {
|
|
669
|
+
id String @id
|
|
670
|
+
tenantId String
|
|
671
|
+
email String
|
|
672
|
+
role String
|
|
673
|
+
token String @unique
|
|
674
|
+
invitedBy String?
|
|
675
|
+
expiresAt DateTime
|
|
676
|
+
acceptedAt DateTime?
|
|
677
|
+
revokedAt DateTime?
|
|
678
|
+
|
|
679
|
+
@@index([tenantId, email])
|
|
680
|
+
@@map("team_invitations")
|
|
681
|
+
}`,
|
|
682
|
+
subscriptions: `model Subscription {
|
|
683
|
+
billableId String @id
|
|
684
|
+
plan String
|
|
685
|
+
period String
|
|
686
|
+
status String
|
|
687
|
+
trialEndsAt DateTime?
|
|
688
|
+
cancelAtPeriodEnd Boolean?
|
|
689
|
+
canceledAt DateTime?
|
|
690
|
+
gatewayRef String?
|
|
691
|
+
pendingPlan String?
|
|
692
|
+
pendingPeriod String?
|
|
693
|
+
|
|
694
|
+
@@map("subscriptions")
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
model UsageCounter {
|
|
698
|
+
billableId String
|
|
699
|
+
feature String
|
|
700
|
+
periodKey String
|
|
701
|
+
value Int @default(0)
|
|
702
|
+
|
|
703
|
+
@@id([billableId, feature, periodKey])
|
|
704
|
+
@@map("usage_counters")
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
model WebhookEvent {
|
|
708
|
+
id String @id
|
|
709
|
+
seenAt DateTime @default(now())
|
|
710
|
+
|
|
711
|
+
@@map("webhook_events")
|
|
712
|
+
}`,
|
|
713
|
+
};
|
|
714
|
+
/**
|
|
715
|
+
* `prisma/schema.prisma` — the datasource, the client generator, the models of
|
|
716
|
+
* every Basalt domain this app enables, and the app's own first model.
|
|
717
|
+
*
|
|
718
|
+
* Prisma 7 no longer takes the connection URL here (prisma.config.ts does), and
|
|
719
|
+
* the client needs an explicit `output`; src/db.ts imports it from there.
|
|
720
|
+
*/
|
|
721
|
+
export function prismaSchema(options) {
|
|
722
|
+
const domains = [];
|
|
723
|
+
if (options.tenancy)
|
|
724
|
+
domains.push(PRISMA_MODELS.tenancy);
|
|
725
|
+
if (options.auth)
|
|
726
|
+
domains.push(PRISMA_MODELS.auth);
|
|
727
|
+
if (enforcesMembership(options))
|
|
728
|
+
domains.push(PRISMA_MODELS.teams);
|
|
729
|
+
if (options.billing)
|
|
730
|
+
domains.push(PRISMA_MODELS.subscriptions);
|
|
731
|
+
return `// Prisma schema for ${options.name}.
|
|
732
|
+
//
|
|
733
|
+
// The blocks below the header are the reference models of the @basaltkit/*-prisma
|
|
734
|
+
// packages this app uses — exactly what \`basalt prisma:sync\` merges. Add another
|
|
735
|
+
// Basalt domain later by installing its \`*-prisma\` package and running
|
|
736
|
+
// \`pnpm basalt prisma:sync\` (with --cli), then \`pnpm db:migrate\`.
|
|
737
|
+
//
|
|
738
|
+
// Apply changes with \`pnpm db:migrate\` (\`prisma migrate dev\`) — never
|
|
739
|
+
// \`prisma db push\`: push writes no _prisma_migrations table, and src/app.ts
|
|
740
|
+
// boots with prismaPlugin({ assertMigrated: true }), which requires it.
|
|
741
|
+
|
|
742
|
+
generator client {
|
|
743
|
+
provider = "prisma-client-js"
|
|
744
|
+
output = "../src/generated/prisma"
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
datasource db {
|
|
748
|
+
// The URL lives in prisma.config.ts (Prisma 7) — and in src/env.ts for the app.
|
|
749
|
+
provider = "postgresql"
|
|
750
|
+
}
|
|
751
|
+
${domains.map((block) => `\n${block}\n`).join('')}
|
|
752
|
+
// ---------------------------------------------------------------------------
|
|
753
|
+
// Your models.${options.tenancy
|
|
754
|
+
? ` \`tenantId\` is the column tenancyExtension() (src/db.ts)
|
|
755
|
+
// forces onto every query and every write, so keep it on everything a tenant
|
|
756
|
+
// owns — and make cross-model foreign keys composite (@@unique([tenantId, id])
|
|
757
|
+
// on the target) so the database refuses a cross-tenant link too.`
|
|
758
|
+
: ''}
|
|
759
|
+
|
|
760
|
+
model Project {
|
|
761
|
+
id String @id @default(uuid())${options.tenancy ? '\n tenantId String' : ''}
|
|
762
|
+
name String
|
|
763
|
+
createdAt DateTime @default(now())
|
|
764
|
+
${options.tenancy ? '\n @@index([tenantId])' : ''}
|
|
765
|
+
@@map("projects")
|
|
766
|
+
}
|
|
767
|
+
`;
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* `prisma.config.ts` — Prisma 7 reads the connection URL (and the seed command)
|
|
771
|
+
* from here instead of from schema.prisma. It applies the SAME precedence as
|
|
772
|
+
* src/env.ts: the app-prefixed name first, the bare `DATABASE_URL` only as a
|
|
773
|
+
* fallback, so the CLI and the running app can never mean different databases.
|
|
774
|
+
*/
|
|
775
|
+
export function prismaConfigTs(options) {
|
|
776
|
+
const prefix = envPrefix(options.name);
|
|
777
|
+
return `import { loadEnvFile } from 'node:process'
|
|
778
|
+
import { defineConfig } from 'prisma/config'
|
|
779
|
+
|
|
780
|
+
// Prisma loads no .env for you (neither does the app — see src/env.ts). This
|
|
781
|
+
// mirrors \`node --env-file=.env\`: a variable already exported in your shell
|
|
782
|
+
// still wins, the file only fills in what is missing.
|
|
783
|
+
try {
|
|
784
|
+
loadEnvFile('.env')
|
|
785
|
+
} catch {
|
|
786
|
+
// No .env file — the variables come from the environment.
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
export default defineConfig({
|
|
790
|
+
schema: 'prisma/schema.prisma',${options.tenancy
|
|
791
|
+
? `
|
|
792
|
+
// Runs after \`prisma migrate dev\` / \`prisma migrate reset\`.
|
|
793
|
+
migrations: { seed: 'tsx prisma/seed.ts' },`
|
|
794
|
+
: ''}
|
|
795
|
+
datasource: {
|
|
796
|
+
// ${prefix}_DATABASE_URL first, bare DATABASE_URL only as a fallback: a
|
|
797
|
+
// stray DATABASE_URL from another project must never decide which database
|
|
798
|
+
// gets migrated.
|
|
799
|
+
url: process.env['${prefix}_DATABASE_URL'] ?? process.env['DATABASE_URL'] ?? '',
|
|
800
|
+
},
|
|
801
|
+
})
|
|
802
|
+
`;
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* `src/db.ts` — the Prisma client(s). Two bindings on purpose: the unscoped
|
|
806
|
+
* `prisma` for the framework stores (they key their own rows and run outside a
|
|
807
|
+
* tenant context) and, with tenancy, the `db` client the whole application uses,
|
|
808
|
+
* where every query is filtered by the request's tenant.
|
|
809
|
+
*/
|
|
810
|
+
export function dbTs(options) {
|
|
811
|
+
return `import { PrismaPg } from '@prisma/adapter-pg'${options.tenancy ? `\nimport { tenancyExtension } from '@basaltkit/prisma'` : ''}
|
|
812
|
+
import { PrismaClient } from './generated/prisma/client.js'
|
|
813
|
+
import { env } from './env.js'
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* The base client. Prisma 7 talks to PostgreSQL through a driver adapter, so the
|
|
817
|
+
* connection string is passed here; prisma.config.ts hands the CLI the same one.
|
|
818
|
+
*
|
|
819
|
+
* ${options.tenancy
|
|
820
|
+
? `This client is NOT tenant-scoped: the framework stores (auth, tenancy,
|
|
821
|
+
* teams, …) read and write their own tables, keyed by their own columns, and do
|
|
822
|
+
* it before a tenant is known (a login has no tenant yet). Application code
|
|
823
|
+
* should use \`db\` below — or \`db<PrismaClient>()\` from @basaltkit/prisma.`
|
|
824
|
+
: `Application code can also reach it with \`db<PrismaClient>()\` from
|
|
825
|
+
* @basaltkit/prisma, which returns the client registered by prismaPlugin.`}
|
|
826
|
+
*/
|
|
827
|
+
export const prisma = new PrismaClient({
|
|
828
|
+
adapter: new PrismaPg({ connectionString: env.DATABASE_URL }),
|
|
829
|
+
})
|
|
830
|
+
${options.tenancy
|
|
831
|
+
? `
|
|
832
|
+
/**
|
|
833
|
+
* The application client: tenancyExtension() forces the current tenant's
|
|
834
|
+
* \`tenantId\` onto every read, stamps it onto every write, and fails closed
|
|
835
|
+
* (MissingTenantError) when there is no tenant in context — application code
|
|
836
|
+
* cannot forget the filter. src/app.ts registers it with prismaPlugin, so
|
|
837
|
+
* \`db<PrismaClient>()\` inside a handler returns this one.
|
|
838
|
+
*/
|
|
839
|
+
export const db = prisma.$extends(tenancyExtension())
|
|
840
|
+
`
|
|
841
|
+
: `
|
|
842
|
+
/** Registered by prismaPlugin in src/app.ts. */
|
|
843
|
+
export const db = prisma
|
|
844
|
+
`}`;
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* `prisma/seed.ts` — creates the `demo` tenant the scaffolded resolvers expect
|
|
848
|
+
* (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed` and after
|
|
849
|
+
* `prisma migrate dev` (prisma.config.ts declares it).
|
|
850
|
+
*/
|
|
851
|
+
export function prismaSeedTs(options) {
|
|
852
|
+
return `import { prismaTenantSource } from '@basaltkit/tenancy-prisma'
|
|
853
|
+
import { prisma } from '../src/db.js'
|
|
854
|
+
|
|
855
|
+
// The demo tenant the header and subdomain resolvers look for out of the box.
|
|
856
|
+
// \`save\` is an upsert, so re-seeding is safe. Tenants are open records: add
|
|
857
|
+
// whatever per-tenant fields you like — they round-trip unchanged.
|
|
858
|
+
const tenants = prismaTenantSource(prisma)
|
|
859
|
+
await tenants.save({ id: 'demo', name: 'Demo Tenant' })
|
|
860
|
+
console.log('Seeded tenant "demo" for ${options.name}.')
|
|
861
|
+
|
|
862
|
+
await prisma.$disconnect()
|
|
863
|
+
`;
|
|
864
|
+
}
|
|
426
865
|
export function appTest(options) {
|
|
866
|
+
const prefix = envPrefix(options.name);
|
|
867
|
+
// With a database the app cannot boot without one: src/env.ts requires
|
|
868
|
+
// DATABASE_URL and prismaPlugin({ assertMigrated: true }) checks the schema.
|
|
869
|
+
// So the suite is gated on a configured database and imports src/app.ts
|
|
870
|
+
// lazily — an eager import would throw at module load instead of skipping.
|
|
427
871
|
return `import { describe, expect, it } from 'vitest'
|
|
428
872
|
import { FASTIFY } from '@basaltkit/fastify'
|
|
429
|
-
|
|
873
|
+
${options.prisma
|
|
874
|
+
? `
|
|
875
|
+
// Needs a migrated database: set ${prefix}_DATABASE_URL (see .env.example), then
|
|
876
|
+
// \`pnpm db:migrate${options.tenancy ? ' && pnpm db:seed' : ''}\`. Without one the suite skips instead of failing.
|
|
877
|
+
const database = process.env['${prefix}_DATABASE_URL'] ?? process.env['DATABASE_URL']
|
|
878
|
+
|
|
879
|
+
describe.skipIf(!database)('app', () => {
|
|
880
|
+
it('boots and responds on /health', async () => {
|
|
881
|
+
const { buildApp } = await import('../src/app.js')
|
|
882
|
+
const app = await buildApp({ logLevel: 'silent' }).boot()`
|
|
883
|
+
: `import { buildApp } from '../src/app.js'
|
|
430
884
|
|
|
431
885
|
describe('app', () => {
|
|
432
886
|
it('boots and responds on /health', async () => {
|
|
433
|
-
const app = await buildApp({ logLevel: 'silent' }).boot()
|
|
887
|
+
const app = await buildApp({ logLevel: 'silent' }).boot()`}
|
|
434
888
|
const server = app.container.get(FASTIFY)
|
|
435
889
|
|
|
436
890
|
const index = await server.inject({ method: 'GET', url: '/' })
|
|
@@ -459,6 +913,7 @@ export function readme(options) {
|
|
|
459
913
|
'typed routes with Zod validation',
|
|
460
914
|
'structured logging with request/tenant context',
|
|
461
915
|
'typed domain events',
|
|
916
|
+
...(options.prisma ? ['PostgreSQL persistence through Prisma (migrations + tenant-scoped client)'] : []),
|
|
462
917
|
...(options.tenancy ? ['multi-tenancy (header + subdomain resolvers)'] : []),
|
|
463
918
|
...(options.auth ? ['authentication (register/login/refresh/me)'] : []),
|
|
464
919
|
...(options.billing ? ['subscriptions with plans and feature limits'] : []),
|
|
@@ -475,11 +930,64 @@ Included: ${features.join(' · ')}.
|
|
|
475
930
|
## Getting started
|
|
476
931
|
|
|
477
932
|
\`\`\`bash
|
|
478
|
-
pnpm install
|
|
933
|
+
pnpm install${options.prisma
|
|
934
|
+
? `
|
|
935
|
+
# point ${envPrefix(options.name)}_DATABASE_URL at a PostgreSQL database (.env.example), then
|
|
936
|
+
pnpm db:migrate # create the tables${options.tenancy ? ` (runs the seed too)` : ''}`
|
|
937
|
+
: ''}
|
|
479
938
|
pnpm dev # API on http://localhost:3000
|
|
480
939
|
pnpm test
|
|
481
940
|
\`\`\`
|
|
941
|
+
${options.prisma
|
|
942
|
+
? `
|
|
943
|
+
## Database
|
|
944
|
+
|
|
945
|
+
PostgreSQL through [Prisma](https://www.prisma.io). \`prisma/schema.prisma\` holds
|
|
946
|
+
the models this app needs — the reference models of the \`@basaltkit/*-prisma\`
|
|
947
|
+
packages it uses, plus your own \`Project\` at the end.
|
|
948
|
+
|
|
949
|
+
\`\`\`bash
|
|
950
|
+
pnpm db:migrate # prisma migrate dev — change the schema, write a migration${options.tenancy
|
|
951
|
+
? `
|
|
952
|
+
pnpm db:seed # the 'demo' tenant the resolvers expect (also run by db:migrate)`
|
|
953
|
+
: ''}
|
|
954
|
+
pnpm db:generate # prisma generate — refresh src/generated/prisma (also runs on install)
|
|
955
|
+
pnpm db:deploy # prisma migrate deploy — apply pending migrations in production
|
|
956
|
+
\`\`\`
|
|
957
|
+
|
|
958
|
+
**Never use \`prisma db push\` on this project.** \`src/app.ts\` boots with
|
|
959
|
+
\`prismaPlugin({ client: db, assertMigrated: true })\`, which refuses to start
|
|
960
|
+
unless the database it reached has the \`_prisma_migrations\` table — the one
|
|
961
|
+
\`migrate dev\` / \`migrate deploy\` write and \`db push\` does not. That check is
|
|
962
|
+
what turns "wrong \`DATABASE_URL\`" (a shell that exported another project's, a
|
|
963
|
+
typo in the database name) into a boot error naming the database and host,
|
|
964
|
+
instead of a 500 on the first request that touches a missing table.
|
|
965
|
+
|
|
966
|
+
\`src/db.ts\` exports two clients:
|
|
482
967
|
|
|
968
|
+
- \`prisma\` — unscoped. The framework stores (auth${options.tenancy ? ', tenancy' : ''}${enforcesMembership(options) ? ', teams' : ''}${options.billing ? ', subscriptions' : ''}) use it: they key
|
|
969
|
+
their own tables and run before a tenant is known.${options.tenancy
|
|
970
|
+
? `
|
|
971
|
+
- \`db\` — \`prisma.$extends(tenancyExtension())\`. Registered with \`prismaPlugin\`,
|
|
972
|
+
so \`db<PrismaClient>()\` inside a handler returns a client that filters every
|
|
973
|
+
read by the request's tenant, stamps every write, and throws instead of running
|
|
974
|
+
unscoped. Keep \`tenantId\` on every model a tenant owns.`
|
|
975
|
+
: ''}
|
|
976
|
+
${options.cli
|
|
977
|
+
? `
|
|
978
|
+
Adding another Basalt domain later (audit, comments, notifications, …): install
|
|
979
|
+
its \`@basaltkit/<domain>-prisma\` package, run \`pnpm basalt prisma:sync\` to merge
|
|
980
|
+
its models into the schema, then \`pnpm db:migrate\`.
|
|
981
|
+
`
|
|
982
|
+
: `
|
|
983
|
+
Adding another Basalt domain later (audit, comments, notifications, …): install
|
|
984
|
+
its \`@basaltkit/<domain>-prisma\` package, copy the models from its
|
|
985
|
+
\`schema.prisma\` into yours, then \`pnpm db:migrate\`. (With the \`basalt\` CLI —
|
|
986
|
+
scaffold with \`--cli\` — \`pnpm basalt prisma:sync\` does the merge for you.)
|
|
987
|
+
`}
|
|
988
|
+
\`pnpm test\` skips the generated suite when no database is configured.
|
|
989
|
+
`
|
|
990
|
+
: ''}
|
|
483
991
|
## Environment
|
|
484
992
|
|
|
485
993
|
\`src/env.ts\` validates \`process.env\` — nothing loads \`.env\` for you. Copy
|
|
@@ -487,7 +995,7 @@ pnpm test
|
|
|
487
995
|
export the variables.
|
|
488
996
|
|
|
489
997
|
The variables are **app-prefixed**: \`${envPrefix(options.name)}_PORT\`,
|
|
490
|
-
\`${envPrefix(options.name)}_HOST\`, \`${envPrefix(options.name)}_LOG_LEVEL\`${options.auth ? `, \`${envPrefix(options.name)}_APP_SECRET\`` : ''}. \`NODE_ENV\` is never prefixed.
|
|
998
|
+
\`${envPrefix(options.name)}_HOST\`, \`${envPrefix(options.name)}_LOG_LEVEL\`${options.auth ? `, \`${envPrefix(options.name)}_APP_SECRET\`` : ''}${options.prisma ? `, \`${envPrefix(options.name)}_DATABASE_URL\`` : ''}. \`NODE_ENV\` is never prefixed.
|
|
491
999
|
|
|
492
1000
|
Why: **\`--env-file\` never overrides a variable that is already exported.** In a
|
|
493
1001
|
shell where another project exported \`DATABASE_URL\` (or \`PORT\`), an app reading
|
|
@@ -495,8 +1003,13 @@ the generic name boots against THAT value and only fails on the first request
|
|
|
495
1003
|
that touches it. \`src/env.ts\` therefore passes \`prefix: '${envPrefix(options.name)}'\` to
|
|
496
1004
|
\`defineEnv\`: each variable is read as \`${envPrefix(options.name)}_<NAME>\` first and
|
|
497
1005
|
**falls back** to the bare \`<NAME>\`, so a deployment that already exports the
|
|
498
|
-
generic names keeps booting while a stray one in your shell loses.
|
|
499
|
-
|
|
1006
|
+
generic names keeps booting while a stray one in your shell loses.${options.prisma
|
|
1007
|
+
? ` That is
|
|
1008
|
+
exactly how \`DATABASE_URL\` is read here (\`${envPrefix(options.name)}_DATABASE_URL\` first), and
|
|
1009
|
+
\`prisma.config.ts\` applies the same precedence — so \`pnpm db:migrate\` and the
|
|
1010
|
+
running app can never mean two different databases.`
|
|
1011
|
+
: ` Add a
|
|
1012
|
+
database and \`DATABASE_URL\` is read as \`${envPrefix(options.name)}_DATABASE_URL\`.`}
|
|
500
1013
|
|
|
501
1014
|
To drop the fallback entirely (prefixed names only), write
|
|
502
1015
|
\`prefix: { value: '${envPrefix(options.name)}', fallback: false }\`. When in doubt about what
|
|
@@ -607,14 +1120,18 @@ export function dockerignore() {
|
|
|
607
1120
|
**/.DS_Store
|
|
608
1121
|
`;
|
|
609
1122
|
}
|
|
610
|
-
export function gitignore() {
|
|
1123
|
+
export function gitignore(options) {
|
|
611
1124
|
return `node_modules/
|
|
612
1125
|
dist/
|
|
613
1126
|
.env
|
|
614
1127
|
.env.*
|
|
615
1128
|
!.env.example
|
|
616
1129
|
*.log
|
|
617
|
-
|
|
1130
|
+
${options.prisma
|
|
1131
|
+
? `# Generated by \`prisma generate\` (runs on install) — never edit or commit it.
|
|
1132
|
+
src/generated/
|
|
1133
|
+
`
|
|
1134
|
+
: ''}`;
|
|
618
1135
|
}
|
|
619
1136
|
/**
|
|
620
1137
|
* pnpm settings: esbuild's build script is required by tsx;
|
package/dist/versions.js
CHANGED
|
@@ -6,19 +6,23 @@ export const SCAFFOLD_VERSIONS = {
|
|
|
6
6
|
'@basaltkit/admin-shadcn': '^1.1.0',
|
|
7
7
|
'@basaltkit/ai-mcp': '^0.1.0',
|
|
8
8
|
'@basaltkit/auth': '^3.1.0',
|
|
9
|
+
'@basaltkit/auth-prisma': '^1.6.0',
|
|
9
10
|
'@basaltkit/cli': '^1.2.0',
|
|
10
11
|
'@basaltkit/config': '^1.1.0',
|
|
11
12
|
'@basaltkit/core': '^1.4.0',
|
|
12
13
|
'@basaltkit/env': '^3.1.0',
|
|
13
14
|
'@basaltkit/events': '^1.3.0',
|
|
14
|
-
'@basaltkit/fastify': '^2.
|
|
15
|
+
'@basaltkit/fastify': '^2.3.0',
|
|
15
16
|
'@basaltkit/generator': '^1.5.0',
|
|
16
17
|
'@basaltkit/logger': '^1.3.0',
|
|
17
18
|
'@basaltkit/mcp': '^3.0.0',
|
|
18
|
-
'@basaltkit/prisma': '^2.
|
|
19
|
+
'@basaltkit/prisma': '^2.2.0',
|
|
19
20
|
'@basaltkit/sdk': '^2.1.0',
|
|
20
21
|
'@basaltkit/subscriptions': '^4.0.0',
|
|
22
|
+
'@basaltkit/subscriptions-prisma': '^2.2.0',
|
|
21
23
|
'@basaltkit/teams': '^3.0.0',
|
|
24
|
+
'@basaltkit/teams-prisma': '^1.0.0',
|
|
22
25
|
'@basaltkit/tenancy': '^2.0.0',
|
|
26
|
+
'@basaltkit/tenancy-prisma': '^1.2.0',
|
|
23
27
|
'@basaltkit/testing': '^2.0.0',
|
|
24
28
|
};
|
package/dist/wizard.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { PackageManager } from './index.js';
|
|
2
2
|
import { type Choice, type Prompter } from './prompt.js';
|
|
3
|
-
export type FeatureKey = 'tenancy' | 'auth' | 'billing' | 'ui' | 'cli' | 'mcp';
|
|
3
|
+
export type FeatureKey = 'tenancy' | 'auth' | 'billing' | 'ui' | 'cli' | 'mcp' | 'prisma';
|
|
4
4
|
/** The feature toggles offered in the "custom" preset (and shown in the summary). */
|
|
5
5
|
export declare const FEATURES: Choice<FeatureKey>[];
|
|
6
6
|
interface Preset {
|
|
@@ -21,6 +21,7 @@ export interface WizardResult {
|
|
|
21
21
|
ui: boolean;
|
|
22
22
|
cli: boolean;
|
|
23
23
|
mcp: boolean;
|
|
24
|
+
prisma: boolean;
|
|
24
25
|
pm: PackageManager;
|
|
25
26
|
install: boolean;
|
|
26
27
|
git: boolean;
|
package/dist/wizard.js
CHANGED
|
@@ -7,11 +7,12 @@ export const FEATURES = [
|
|
|
7
7
|
{ value: 'ui', label: 'Web UI', hint: 'React + shadcn + SDK (pnpm workspace)' },
|
|
8
8
|
{ value: 'cli', label: 'basalt CLI', hint: 'code generators + commands' },
|
|
9
9
|
{ value: 'mcp', label: 'MCP server', hint: 'expose routes as AI-agent tools (/mcp)' },
|
|
10
|
+
{ value: 'prisma', label: 'Database', hint: 'PostgreSQL via Prisma (migrations + real stores)' },
|
|
10
11
|
];
|
|
11
12
|
export const PRESETS = [
|
|
12
|
-
{ value: 'saas', label: 'SaaS starter', hint: 'tenancy + auth + billing + CLI', features: ['tenancy', 'auth', 'billing', 'cli'] },
|
|
13
|
+
{ value: 'saas', label: 'SaaS starter', hint: 'tenancy + auth + billing + database + CLI', features: ['tenancy', 'auth', 'billing', 'prisma', 'cli'] },
|
|
13
14
|
{ value: 'api', label: 'API only', hint: 'auth + MCP, no tenancy or UI', features: ['auth', 'mcp'] },
|
|
14
|
-
{ value: 'full', label: 'Full stack', hint: 'everything + web UI', features: ['tenancy', 'auth', 'billing', 'ui', 'cli', 'mcp'] },
|
|
15
|
+
{ value: 'full', label: 'Full stack', hint: 'everything + web UI', features: ['tenancy', 'auth', 'billing', 'prisma', 'ui', 'cli', 'mcp'] },
|
|
15
16
|
{ value: 'minimal', label: 'Minimal', hint: 'no batteries, add them later', features: [] },
|
|
16
17
|
{ value: 'custom', label: 'Custom', hint: 'pick features yourself', features: null },
|
|
17
18
|
];
|
|
@@ -90,6 +91,7 @@ export async function runWizard(prompter, options = {}) {
|
|
|
90
91
|
ui: featureSet.has('ui'),
|
|
91
92
|
cli: featureSet.has('cli'),
|
|
92
93
|
mcp: featureSet.has('mcp'),
|
|
94
|
+
prisma: featureSet.has('prisma'),
|
|
93
95
|
pm,
|
|
94
96
|
install,
|
|
95
97
|
git,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-basalt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=22.5.0"
|
|
6
6
|
},
|
|
@@ -21,26 +21,35 @@
|
|
|
21
21
|
"dist"
|
|
22
22
|
],
|
|
23
23
|
"devDependencies": {
|
|
24
|
+
"@basaltkit/auth-prisma": "^1.5.1",
|
|
25
|
+
"@basaltkit/subscriptions-prisma": "^2.2.3",
|
|
26
|
+
"@basaltkit/teams-prisma": "^1.0.3",
|
|
27
|
+
"@basaltkit/tenancy-prisma": "^1.2.0",
|
|
28
|
+
"@prisma/adapter-pg": "^7.10.0",
|
|
29
|
+
"@prisma/client": "^7.10.0",
|
|
24
30
|
"@types/node": "^26.6.1",
|
|
31
|
+
"@types/pg": "^8.23.1",
|
|
32
|
+
"pg": "^8.23.0",
|
|
33
|
+
"prisma": "^7.10.0",
|
|
25
34
|
"typescript": "^7.0.2",
|
|
26
35
|
"vitest": "^5.0.1",
|
|
27
36
|
"zod": "^4.6.5",
|
|
28
|
-
"@basaltkit/config": "^1.1.2",
|
|
29
37
|
"@basaltkit/auth": "^3.1.0",
|
|
30
38
|
"@basaltkit/cli": "^1.2.5",
|
|
31
|
-
"@basaltkit/
|
|
39
|
+
"@basaltkit/config": "^1.1.2",
|
|
32
40
|
"@basaltkit/core": "^1.4.0",
|
|
41
|
+
"@basaltkit/env": "^3.1.0",
|
|
33
42
|
"@basaltkit/events": "^1.3.0",
|
|
34
|
-
"@basaltkit/fastify": "^2.2.0",
|
|
35
43
|
"@basaltkit/generator": "^1.5.0",
|
|
44
|
+
"@basaltkit/fastify": "^2.3.0",
|
|
36
45
|
"@basaltkit/logger": "^1.3.0",
|
|
37
46
|
"@basaltkit/mcp": "^3.0.0",
|
|
47
|
+
"@basaltkit/prisma": "^2.2.0",
|
|
38
48
|
"@basaltkit/subscriptions": "^4.0.0",
|
|
39
49
|
"@basaltkit/teams": "^3.0.1",
|
|
40
50
|
"@basaltkit/tenancy": "^2.0.0",
|
|
41
|
-
"@basaltkit/tsconfig": "^0.24.0",
|
|
42
51
|
"@basaltkit/testing": "^2.0.2",
|
|
43
|
-
"@basaltkit/
|
|
52
|
+
"@basaltkit/tsconfig": "^0.24.0"
|
|
44
53
|
},
|
|
45
54
|
"publishConfig": {
|
|
46
55
|
"access": "public"
|