create-basalt 1.11.0 → 1.13.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/dist/stubs.js ADDED
@@ -0,0 +1,123 @@
1
+ // GENERATED by scripts/sync-stubs.mjs from packages/cli/src/stubs.ts (runs before every build) — do not edit.
2
+ /**
3
+ * The production stubs `basalt publish` copies into an app — the SINGLE source
4
+ * of truth. create-basalt ships the same files in every new scaffold and offers
5
+ * them from `create-basalt update`: its build copies this file verbatim to
6
+ * `packages/create-app/src/stubs.ts` (scripts/sync-stubs.mjs), and a test there
7
+ * fails if the copy drifts. Keep this module free of imports for that reason.
8
+ *
9
+ * The layout these stubs assume is the create-basalt scaffold's (BK-026):
10
+ * `pnpm build` runs `tsc -p tsconfig.build.json` (rootDir `.`, outDir `dist`),
11
+ * so `src/server.ts` compiles to {@link PRODUCTION_ENTRY}; `pnpm start` runs it
12
+ * with plain `node` — no tsx, no TypeScript at runtime. A Prisma client, when
13
+ * there is one, is generated OUTSIDE `src/` (`generated/prisma`) and imported
14
+ * through the package.json `imports` alias `#db/*`, so it resolves the same
15
+ * from `src/` (tsx) and from `dist/src/` (node).
16
+ */
17
+ /** The compiled server entry the Dockerfile runs and the scaffold's `start` script points at. */
18
+ export const PRODUCTION_ENTRY = 'dist/src/server.js';
19
+ /** Multi-stage production image: build with the dev toolchain, run on plain node as a non-root user. */
20
+ export const DOCKERFILE = `# syntax=docker/dockerfile:1
21
+ # Production image for a Basalt app (the create-basalt layout): \`pnpm build\`
22
+ # compiles src/ to dist/ and the image runs ${PRODUCTION_ENTRY} on plain
23
+ # node — no tsx, no TypeScript at runtime.
24
+ #
25
+ # docker build -t my-app .
26
+ # docker run -p 3000:3000 -e APP_SECRET=… -e DATABASE_URL=… my-app
27
+ #
28
+ # Configuration comes from the environment (nothing loads .env here, and
29
+ # .dockerignore keeps it out of the image). Database migrations are NOT run
30
+ # by the image: apply them before rolling out (\`pnpm db:deploy\` in CI).
31
+ #
32
+ # Installs with pnpm from pnpm-lock.yaml. An app on npm/yarn/bun replaces the
33
+ # pnpm install / exec / run / prune lines with its own package manager's.
34
+ ARG NODE_VERSION=22
35
+ ARG PNPM_VERSION=11
36
+
37
+ FROM node:\${NODE_VERSION}-slim AS build
38
+ ARG PNPM_VERSION
39
+ # Non-interactive pnpm: without it \`pnpm prune\` in a workspace (a --ui app's
40
+ # web/) aborts with ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY.
41
+ ENV CI=true
42
+ WORKDIR /app
43
+ RUN npm install -g pnpm@\${PNPM_VERSION}
44
+ COPY . .
45
+ # Full install: the build needs the devDependencies (typescript, prisma).
46
+ RUN pnpm install --frozen-lockfile
47
+ # The Prisma client (when the app has a schema) is generated into ./generated.
48
+ RUN if [ -f prisma/schema.prisma ]; then pnpm exec prisma generate; fi && mkdir -p generated
49
+ RUN pnpm run build
50
+ # Drop the devDependencies (tsx, typescript, prisma CLI, generators, the AI
51
+ # bridge): none of them may ship. --ignore-scripts keeps the app's postinstall
52
+ # (\`prisma generate\`, a dev tool) from running again.
53
+ RUN pnpm prune --prod --ignore-scripts
54
+
55
+ FROM node:\${NODE_VERSION}-slim AS run
56
+ ENV NODE_ENV=production
57
+ ENV PORT=3000
58
+ # Path the HEALTHCHECK probes: the scaffold's /health route by default; set
59
+ # /readyz when the app registers healthPlugin().
60
+ ENV HEALTHCHECK_PATH=/health
61
+ WORKDIR /app
62
+ COPY --from=build --chown=node:node /app/package.json ./package.json
63
+ COPY --from=build --chown=node:node /app/node_modules ./node_modules
64
+ COPY --from=build --chown=node:node /app/generated ./generated
65
+ COPY --from=build --chown=node:node /app/dist ./dist
66
+ USER node
67
+ EXPOSE 3000
68
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \\
69
+ CMD node -e "fetch('http://127.0.0.1:' + (process.env.PORT || 3000) + process.env.HEALTHCHECK_PATH).then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"
70
+ CMD ["node", "--enable-source-maps", "${PRODUCTION_ENTRY}"]
71
+ `;
72
+ /**
73
+ * Published with the Dockerfile: its \`COPY . .\` would otherwise bake \`.env\`,
74
+ * private keys and VCS metadata into an image layer. Every secret rule is
75
+ * \`**\/\`-prefixed: .dockerignore patterns are anchored at the context root, so
76
+ * a bare \`.env\` or \`*.pem\` would still let \`prisma/.env\` or
77
+ * \`certs/server.key\` through. \`dist\` is rebuilt inside the image.
78
+ */
79
+ export const DOCKERIGNORE = `**/.env
80
+ **/.env.*
81
+ !**/.env.example
82
+ **/.npmrc
83
+ **/.git
84
+ **/node_modules
85
+ **/coverage
86
+ **/*.log
87
+ **/*.pem
88
+ **/*.key
89
+ **/*.p12
90
+ **/*.pfx
91
+ **/.DS_Store
92
+ dist
93
+ `;
94
+ /** GitHub Actions CI: install, typecheck, build (the production path), test. */
95
+ export const CI_WORKFLOW = `name: ci
96
+ on:
97
+ push: { branches: [main] }
98
+ pull_request:
99
+ jobs:
100
+ build:
101
+ runs-on: ubuntu-latest
102
+ steps:
103
+ - uses: actions/checkout@v4
104
+ # Drop \`with: version\` if package.json declares a packageManager.
105
+ - uses: pnpm/action-setup@v4
106
+ with: { version: 11 }
107
+ - uses: actions/setup-node@v4
108
+ with: { node-version: 22, cache: pnpm }
109
+ - run: pnpm install --frozen-lockfile
110
+ - run: pnpm run typecheck
111
+ - run: pnpm run build
112
+ - run: pnpm run test
113
+ `;
114
+ export const EDITORCONFIG = `root = true
115
+
116
+ [*]
117
+ charset = utf-8
118
+ end_of_line = lf
119
+ insert_final_newline = true
120
+ indent_style = space
121
+ indent_size = 2
122
+ trim_trailing_whitespace = true
123
+ `;
@@ -20,8 +20,8 @@ export { basaltBin } from './bin-template.js';
20
20
  export { SCAFFOLD_VERSIONS } from './versions.js';
21
21
  export { THIRD_PARTY_VERSIONS } from './latest-versions.js';
22
22
  /**
23
- * The dependency range for a @basaltkit/* package: the current release line
24
- * (`^<major>.<minor>.0`) of its workspace version, from the generated
23
+ * The dependency range for a @basaltkit/* package: its workspace version as
24
+ * the floor (`^<major>.<minor>.<patch>`), from the generated
25
25
  * {@link SCAFFOLD_VERSIONS} map. Throws for an unknown package rather than
26
26
  * guessing a range — a stale guess once pinned new apps to frozen 1.x lines.
27
27
  */
@@ -33,6 +33,24 @@ export declare const versionOf: (pkg: string) => string;
33
33
  * unknown package so every emitted range lives in that single table.
34
34
  */
35
35
  export declare const thirdPartyVersionOf: (pkg: string) => string;
36
+ /**
37
+ * The production path (BK-026): `build` compiles src/ with tsconfig.build.json
38
+ * (rootDir `.`, outDir `dist`) and `start` runs the result on plain node — the
39
+ * same entry the Dockerfile's CMD runs. `start:dev` keeps the old tsx start.
40
+ */
41
+ export declare const BUILD_SCRIPT = "tsc -p tsconfig.build.json";
42
+ /** The `db:seed` script of a `--prisma` scaffold with tenancy (prisma.config.ts loads .env, then runs `migrations.seed`). */
43
+ export declare const DB_SEED_SCRIPT = "prisma db seed";
44
+ export declare const START_SCRIPT = "node --enable-source-maps dist/src/server.js";
45
+ export declare const START_DEV_SCRIPT = "tsx src/server.ts";
46
+ /** The `start` script every create-basalt release before the production path wrote. */
47
+ export declare const LEGACY_START_SCRIPT = "tsx src/server.ts";
48
+ /**
49
+ * package.json `imports` alias for the generated Prisma client. It lives in
50
+ * ./generated/prisma — outside src/, so tsc never has to copy its .js files —
51
+ * and `#db/*` resolves to it from src/ (tsx, vitest) and dist/src/ (node) alike.
52
+ */
53
+ export declare const PRISMA_IMPORTS: Readonly<Record<string, string>>;
36
54
  /** The root `basalt` script of an app without `--cli`: create-basalt's project commands. */
37
55
  export declare const BASALT_PROJECT_SCRIPT = "create-basalt --project";
38
56
  /** The root `dev:web` script of a `--ui` app (web/ is a pnpm workspace member). */
@@ -41,6 +59,15 @@ export declare const webDevScript: (options: Pick<ProjectOptions, 'name'>) => st
41
59
  export declare const enforcesMembership: (options: ProjectOptions) => boolean;
42
60
  export declare function packageJson(options: ProjectOptions): string;
43
61
  export declare function tsconfigJson(options: ProjectOptions): string;
62
+ /**
63
+ * `tsconfig.build.json` — what `pnpm build` compiles: src/ only (tests and
64
+ * bin/ stay dev-only), emitted to dist/ with rootDir `.`, so src/server.ts
65
+ * becomes dist/src/server.js. Extends tsconfig.json, so the compiler options
66
+ * (and `pnpm typecheck`) never diverge.
67
+ */
68
+ export declare function tsconfigBuildJson(): string;
69
+ /** The production Dockerfile — the same file `basalt publish dockerfile` writes. */
70
+ export declare function dockerfile(): string;
44
71
  export declare function envTs(options: ProjectOptions): string;
45
72
  /**
46
73
  * An app-specific env-var prefix derived from the project name
@@ -101,8 +128,9 @@ export declare function prismaConfigTs(options: ProjectOptions): string;
101
128
  export declare function dbTs(options: ProjectOptions): string;
102
129
  /**
103
130
  * `prisma/seed.ts` — creates the `demo` tenant the scaffolded resolvers expect
104
- * (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed` and after
105
- * `prisma migrate dev` (prisma.config.ts declares it).
131
+ * (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed`
132
+ * (`prisma db seed`, the command prisma.config.ts declares) — Prisma 7's
133
+ * `migrate dev` does not run it.
106
134
  */
107
135
  export declare function prismaSeedTs(options: ProjectOptions): string;
108
136
  export declare function appTest(options: ProjectOptions): string;
@@ -129,7 +157,9 @@ export declare function mcpJson(_options: ProjectOptions): string;
129
157
  export declare function dockerignore(): string;
130
158
  export declare function gitignore(options: ProjectOptions): string;
131
159
  /**
132
- * pnpm settings: esbuild's build script is required by tsx;
160
+ * pnpm settings: esbuild's build script is required by tsx; with --prisma,
161
+ * the Prisma CLI's build scripts (prisma, @prisma/engines) are approved too — pnpm 11 fails
162
+ * the install on an unapproved build;
133
163
  * msgpackr-extract (optional native accelerator via BullMQ) is declined —
134
164
  * msgpackr falls back to pure JS. With --ui, `web` is a workspace member so
135
165
  * its own dependencies resolve.
package/dist/templates.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { THIRD_PARTY_VERSIONS } from './latest-versions.js';
2
+ import { DOCKERFILE, DOCKERIGNORE, PRODUCTION_ENTRY } from './stubs.js';
2
3
  import { SCAFFOLD_VERSIONS } from './versions.js';
3
4
  export { basaltBin } from './bin-template.js';
4
5
  export { SCAFFOLD_VERSIONS } from './versions.js';
5
6
  export { THIRD_PARTY_VERSIONS } from './latest-versions.js';
6
7
  /**
7
- * The dependency range for a @basaltkit/* package: the current release line
8
- * (`^<major>.<minor>.0`) of its workspace version, from the generated
8
+ * The dependency range for a @basaltkit/* package: its workspace version as
9
+ * the floor (`^<major>.<minor>.<patch>`), from the generated
9
10
  * {@link SCAFFOLD_VERSIONS} map. Throws for an unknown package rather than
10
11
  * guessing a range — a stale guess once pinned new apps to frozen 1.x lines.
11
12
  */
@@ -29,6 +30,24 @@ export const thirdPartyVersionOf = (pkg) => {
29
30
  }
30
31
  return range;
31
32
  };
33
+ /**
34
+ * The production path (BK-026): `build` compiles src/ with tsconfig.build.json
35
+ * (rootDir `.`, outDir `dist`) and `start` runs the result on plain node — the
36
+ * same entry the Dockerfile's CMD runs. `start:dev` keeps the old tsx start.
37
+ */
38
+ export const BUILD_SCRIPT = 'tsc -p tsconfig.build.json';
39
+ /** The `db:seed` script of a `--prisma` scaffold with tenancy (prisma.config.ts loads .env, then runs `migrations.seed`). */
40
+ export const DB_SEED_SCRIPT = 'prisma db seed';
41
+ export const START_SCRIPT = `node --enable-source-maps ${PRODUCTION_ENTRY}`;
42
+ export const START_DEV_SCRIPT = 'tsx src/server.ts';
43
+ /** The `start` script every create-basalt release before the production path wrote. */
44
+ export const LEGACY_START_SCRIPT = 'tsx src/server.ts';
45
+ /**
46
+ * package.json `imports` alias for the generated Prisma client. It lives in
47
+ * ./generated/prisma — outside src/, so tsc never has to copy its .js files —
48
+ * and `#db/*` resolves to it from src/ (tsx, vitest) and dist/src/ (node) alike.
49
+ */
50
+ export const PRISMA_IMPORTS = { '#db/*': './generated/prisma/*' };
32
51
  /** The root `basalt` script of an app without `--cli`: create-basalt's project commands. */
33
52
  export const BASALT_PROJECT_SCRIPT = 'create-basalt --project';
34
53
  /** The root `dev:web` script of a `--ui` app (web/ is a pnpm workspace member). */
@@ -75,12 +94,20 @@ export function packageJson(options) {
75
94
  if (options.prisma) {
76
95
  // Prisma 7 talks to PostgreSQL through a driver adapter, so `pg` is a real
77
96
  // runtime dependency (see src/db.ts); the `prisma` CLI is dev-only.
78
- for (const pkg of ['@prisma/adapter-pg', '@prisma/client', 'pg']) {
97
+ // @prisma/client-runtime-utils is required BY NAME from the generated
98
+ // client's runtime/ (./generated/prisma/runtime/client.js). Under pnpm it is
99
+ // only a transitive dependency of @prisma/client, out of reach of plain
100
+ // `node` — so the app declares it directly (BK-026).
101
+ for (const pkg of ['@prisma/adapter-pg', '@prisma/client', '@prisma/client-runtime-utils', 'pg']) {
79
102
  dependencies[pkg] = thirdPartyVersionOf(pkg);
80
103
  }
81
104
  }
82
105
  const devDependencies = {
83
106
  '@basaltkit/testing': versionOf('@basaltkit/testing'),
107
+ // @basaltkit/testing's peer: fakeMailer() binds the app's own MAILER token,
108
+ // so the copy must be the app's (a package manager that skips peers would
109
+ // otherwise leave `@basaltkit/testing` unable to load).
110
+ '@basaltkit/mailer': versionOf('@basaltkit/mailer'),
84
111
  // The project commands (\`pnpm basalt update | add | doctor | info\`) — dev
85
112
  // tooling with no dependencies of its own, never imported by the app.
86
113
  'create-basalt': versionOf('create-basalt'),
@@ -108,11 +135,15 @@ export function packageJson(options) {
108
135
  version: '0.1.0',
109
136
  private: true,
110
137
  type: 'module',
138
+ ...(options.prisma ? { imports: PRISMA_IMPORTS } : {}),
111
139
  scripts: {
112
140
  // dev opts into NODE_ENV=development (src/dev.ts); start does not, so an
113
141
  // unset NODE_ENV counts as production and secrets are required.
114
142
  dev: 'tsx watch src/dev.ts',
115
- start: 'tsx src/server.ts',
143
+ // Production: compile once, run on plain node (no tsx at runtime).
144
+ build: BUILD_SCRIPT,
145
+ start: START_SCRIPT,
146
+ 'start:dev': START_DEV_SCRIPT,
116
147
  test: 'vitest run',
117
148
  typecheck: 'tsc --noEmit',
118
149
  // With --cli, bin/basalt.ts serves the generators AND hands the project
@@ -132,7 +163,11 @@ export function packageJson(options) {
132
163
  // is deliberately no `db:push` script.
133
164
  'db:migrate': 'prisma migrate dev',
134
165
  'db:deploy': 'prisma migrate deploy',
135
- ...(options.tenancy ? { 'db:seed': 'tsx prisma/seed.ts' } : {}),
166
+ // Through Prisma, not `tsx prisma/seed.ts`: prisma.config.ts loads
167
+ // .env (the seed imports src/env.ts, which needs the database URL),
168
+ // then runs the `migrations.seed` command it declares. Prisma 7's
169
+ // `migrate dev` no longer seeds on its own.
170
+ ...(options.tenancy ? { 'db:seed': DB_SEED_SCRIPT } : {}),
136
171
  }
137
172
  : {}),
138
173
  },
@@ -153,12 +188,34 @@ export function tsconfigJson(options) {
153
188
  noEmit: true,
154
189
  types: ['node'],
155
190
  },
156
- // The generated Prisma client lives under src/generated (git-ignored, and
157
- // re-created by `prisma generate`); the seed script under prisma/ is
158
- // type-checked too.
191
+ // The generated Prisma client lives in ./generated (git-ignored,
192
+ // re-created by `prisma generate`, reached through the `#db/*` import
193
+ // alias); the seed script under prisma/ is type-checked too.
159
194
  include: ['src', 'tests', ...(options.prisma && options.tenancy ? ['prisma/seed.ts'] : [])],
160
195
  }, null, 2)}\n`;
161
196
  }
197
+ /**
198
+ * `tsconfig.build.json` — what `pnpm build` compiles: src/ only (tests and
199
+ * bin/ stay dev-only), emitted to dist/ with rootDir `.`, so src/server.ts
200
+ * becomes dist/src/server.js. Extends tsconfig.json, so the compiler options
201
+ * (and `pnpm typecheck`) never diverge.
202
+ */
203
+ export function tsconfigBuildJson() {
204
+ return `${JSON.stringify({
205
+ extends: './tsconfig.json',
206
+ compilerOptions: {
207
+ noEmit: false,
208
+ rootDir: '.',
209
+ outDir: 'dist',
210
+ sourceMap: true,
211
+ },
212
+ include: ['src'],
213
+ }, null, 2)}\n`;
214
+ }
215
+ /** The production Dockerfile — the same file `basalt publish dockerfile` writes. */
216
+ export function dockerfile() {
217
+ return DOCKERFILE;
218
+ }
162
219
  export function envTs(options) {
163
220
  const prefix = envPrefix(options.name);
164
221
  return `import { defineEnv${options.auth ? ', secret' : ''} } from '@basaltkit/env'
@@ -534,7 +591,15 @@ import { env } from './env.js'
534
591
  const app = await buildApp({
535
592
  logLevel: env.LOG_LEVEL,
536
593
  pretty: env.NODE_ENV === 'development',
537
- }).boot()
594
+ })
595
+ .boot()
596
+ .catch((error: unknown) => {
597
+ // A database failure diagnosed by @basaltkit/prisma (permission denied, not
598
+ // migrated, unreachable…) carries its one-line fix: print it above the stack.
599
+ const diagnosis = (error as { details?: { diagnosis?: { cause?: string; fix?: string } } } | null)?.details?.diagnosis
600
+ if (diagnosis) console.error('Cannot start: ' + diagnosis.cause + '\\nFix: ' + diagnosis.fix)
601
+ throw error
602
+ })
538
603
 
539
604
  const server = app.container.get(FASTIFY)
540
605
  await server.listen({ port: env.PORT, host: env.HOST })
@@ -594,9 +659,17 @@ const PRISMA_MODELS = {
594
659
  }
595
660
 
596
661
  model TenantDomain {
597
- domain String @id
598
- tenantId String
599
- tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
662
+ domain String @id
663
+ tenantId String
664
+ // Custom-domain verification (PrismaDomainStore). A row with a NULL token
665
+ // mirrors \`tenant.domains\` and is written by PrismaTenantSource; a row with a
666
+ // token is a CustomDomains claim, which the source never deletes, and which
667
+ // only resolves a request once \`verified\` is true.
668
+ verificationToken String?
669
+ verified Boolean @default(true)
670
+ createdAt DateTime @default(now())
671
+ verifiedAt DateTime?
672
+ tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
600
673
 
601
674
  @@index([tenantId])
602
675
  @@map("tenant_domains")
@@ -611,9 +684,11 @@ model TenantDomain {
611
684
  }
612
685
 
613
686
  model AuthSession {
614
- id String @id
615
- userId String
616
- expiresAt DateTime
687
+ id String @id
688
+ userId String
689
+ expiresAt DateTime
690
+ /// Last use, for the idle timeout (trackSessionActivity: true).
691
+ lastSeenAt DateTime?
617
692
 
618
693
  @@index([userId])
619
694
  @@map("auth_sessions")
@@ -654,9 +729,17 @@ model AuthApiKey {
654
729
  expiresAt DateTime?
655
730
  lastUsedAt DateTime?
656
731
  revokedAt DateTime?
732
+ // 3.0: API key lifecycle (migration \`add_api_key_lifecycle\`).
733
+ serviceId String?
734
+ previousHash String? @unique
735
+ previousHashExpiresAt DateTime?
736
+ rotatedAt DateTime?
737
+ allowedIps String[] @default([])
738
+ meta Json?
657
739
 
658
740
  @@index([tenantId])
659
741
  @@index([userId])
742
+ @@index([serviceId])
660
743
  @@map("auth_api_keys")
661
744
  }
662
745
 
@@ -792,7 +875,9 @@ export function prismaSchema(options) {
792
875
 
793
876
  generator client {
794
877
  provider = "prisma-client-js"
795
- output = "../src/generated/prisma"
878
+ // Outside src/ on purpose: src/db.ts imports it as \`#db/client.js\` (the
879
+ // package.json "imports" alias), which resolves from src/ and dist/src/ alike.
880
+ output = "../generated/prisma"
796
881
  }
797
882
 
798
883
  datasource db {
@@ -840,7 +925,7 @@ try {
840
925
  export default defineConfig({
841
926
  schema: 'prisma/schema.prisma',${options.tenancy
842
927
  ? `
843
- // Runs after \`prisma migrate dev\` / \`prisma migrate reset\`.
928
+ // Run by \`prisma db seed\` (\`pnpm db:seed\`), with the .env loaded above.
844
929
  migrations: { seed: 'tsx prisma/seed.ts' },`
845
930
  : ''}
846
931
  datasource: {
@@ -860,7 +945,9 @@ export default defineConfig({
860
945
  */
861
946
  export function dbTs(options) {
862
947
  return `import { PrismaPg } from '@prisma/adapter-pg'${options.tenancy ? `\nimport { tenancyExtension } from '@basaltkit/prisma'` : ''}
863
- import { PrismaClient } from './generated/prisma/client.js'
948
+ // The generated client (./generated/prisma), through the package.json
949
+ // "imports" alias — the same specifier works under tsx and from dist/.
950
+ import { PrismaClient } from '#db/client.js'
864
951
  import { env } from './env.js'
865
952
 
866
953
  /**
@@ -896,8 +983,9 @@ export const db = prisma
896
983
  }
897
984
  /**
898
985
  * `prisma/seed.ts` — creates the `demo` tenant the scaffolded resolvers expect
899
- * (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed` and after
900
- * `prisma migrate dev` (prisma.config.ts declares it).
986
+ * (`x-tenant-id: demo`, `demo.localhost`). Runs from `pnpm db:seed`
987
+ * (`prisma db seed`, the command prisma.config.ts declares) — Prisma 7's
988
+ * `migrate dev` does not run it.
901
989
  */
902
990
  export function prismaSeedTs(options) {
903
991
  return `import { prismaTenantSource } from '@basaltkit/tenancy-prisma'
@@ -984,7 +1072,8 @@ Included: ${features.join(' · ')}.
984
1072
  pnpm install${options.prisma
985
1073
  ? `
986
1074
  # start PostgreSQL — .env points ${envPrefix(options.name)}_DATABASE_URL at localhost:5432 — then
987
- pnpm db:migrate # create the tables${options.tenancy ? ` (runs the seed too)` : ''}`
1075
+ pnpm db:migrate # create the tables${options.tenancy ? `
1076
+ pnpm db:seed # the 'demo' tenant the resolvers expect` : ''}`
988
1077
  : ''}
989
1078
  pnpm dev # API on http://localhost:3000
990
1079
  pnpm test
@@ -998,11 +1087,11 @@ the models this app needs — the reference models of the \`@basaltkit/*-prisma\
998
1087
  packages it uses, plus your own \`Project\` at the end.
999
1088
 
1000
1089
  \`\`\`bash
1001
- pnpm db:migrate # prisma migrate dev — change the schema, write a migration${options.tenancy
1090
+ pnpm db:migrate --name <change> # prisma migrate dev — writes AND applies a migration (asks for a name without --name)${options.tenancy
1002
1091
  ? `
1003
- pnpm db:seed # the 'demo' tenant the resolvers expect (also run by db:migrate)`
1092
+ pnpm db:seed # prisma db seed — the 'demo' tenant the resolvers expect`
1004
1093
  : ''}
1005
- pnpm db:generate # prisma generate — refresh src/generated/prisma (also runs on install)
1094
+ pnpm db:generate # prisma generate — refresh generated/prisma after a schema change (also runs on install; migrate dev does not)
1006
1095
  pnpm db:deploy # prisma migrate deploy — apply pending migrations in production
1007
1096
  \`\`\`
1008
1097
 
@@ -1044,7 +1133,7 @@ scaffold with \`--cli\` — \`pnpm basalt prisma:sync\` does the merge for you.)
1044
1133
  \`src/env.ts\` validates \`process.env\`. For development, ${options.cli ? '`pnpm dev` and `pnpm basalt` load' : '`pnpm dev` loads'} \`.env\`
1045
1134
  automatically — create-basalt wrote one for you: git-ignored, with local
1046
1135
  values${options.auth ? ' and a generated secret' : ''}. \`.env.example\` is its committed template.
1047
- \`pnpm start\` (production, \`src/server.ts\`) loads **nothing**: there the
1136
+ \`pnpm start\` (production, \`${PRODUCTION_ENTRY}\`) loads **nothing**: there the
1048
1137
  variables come from the real environment (or \`node --env-file=.env\`).
1049
1138
 
1050
1139
  The variables are **app-prefixed**: \`${envPrefix(options.name)}_PORT\`,
@@ -1068,6 +1157,24 @@ To drop the fallback entirely (prefixed names only), write
1068
1157
  \`prefix: { value: '${envPrefix(options.name)}', fallback: false }\`. When in doubt about what
1069
1158
  your shell exports: \`env | grep DATABASE_URL\`.
1070
1159
 
1160
+ ## Build & ship
1161
+
1162
+ \`\`\`bash
1163
+ pnpm build # tsc -p tsconfig.build.json → dist/ (src only)
1164
+ pnpm start # node --enable-source-maps ${PRODUCTION_ENTRY} — no tsx at runtime
1165
+ pnpm start:dev # the server from source with tsx, without building
1166
+ docker build -t ${options.name} . # the Dockerfile: build stage + plain-node runtime
1167
+ \`\`\`
1168
+
1169
+ \`pnpm start\` runs the compiled server, so build first. The \`Dockerfile\` (also
1170
+ what \`basalt publish dockerfile\` writes) installs, ${options.prisma ? 'generates the Prisma client, ' : ''}builds, drops the
1171
+ devDependencies and runs ${PRODUCTION_ENTRY} as the \`node\` user, with a
1172
+ \`HEALTHCHECK\` on \`/health\`. Pass the configuration as environment variables${options.prisma ? ', and apply migrations (\`pnpm db:deploy\`) before rolling out — the image does not run them' : ''}.${options.prisma
1173
+ ? ` The Prisma client is generated into \`generated/prisma\` (outside \`src/\`) and
1174
+ imported as \`#db/client.js\` through the package.json \`imports\` alias, so the same
1175
+ import works under tsx and from \`dist/\`.`
1176
+ : ''}
1177
+
1071
1178
  ## Keeping it up to date
1072
1179
 
1073
1180
  \`\`\`bash
@@ -1177,20 +1284,7 @@ export function mcpJson(_options) {
1177
1284
  * `certs/server.key` through.
1178
1285
  */
1179
1286
  export function dockerignore() {
1180
- return `**/.env
1181
- **/.env.*
1182
- !**/.env.example
1183
- **/.npmrc
1184
- **/.git
1185
- **/node_modules
1186
- **/coverage
1187
- **/*.log
1188
- **/*.pem
1189
- **/*.key
1190
- **/*.p12
1191
- **/*.pfx
1192
- **/.DS_Store
1193
- `;
1287
+ return DOCKERIGNORE;
1194
1288
  }
1195
1289
  export function gitignore(options) {
1196
1290
  return `node_modules/
@@ -1201,12 +1295,14 @@ dist/
1201
1295
  *.log
1202
1296
  ${options.prisma
1203
1297
  ? `# Generated by \`prisma generate\` (runs on install) — never edit or commit it.
1204
- src/generated/
1298
+ generated/
1205
1299
  `
1206
1300
  : ''}`;
1207
1301
  }
1208
1302
  /**
1209
- * pnpm settings: esbuild's build script is required by tsx;
1303
+ * pnpm settings: esbuild's build script is required by tsx; with --prisma,
1304
+ * the Prisma CLI's build scripts (prisma, @prisma/engines) are approved too — pnpm 11 fails
1305
+ * the install on an unapproved build;
1210
1306
  * msgpackr-extract (optional native accelerator via BullMQ) is declined —
1211
1307
  * msgpackr falls back to pure JS. With --ui, `web` is a workspace member so
1212
1308
  * its own dependencies resolve.
@@ -1224,7 +1320,13 @@ export function pnpmWorkspaceYaml(options) {
1224
1320
  `
1225
1321
  : ''}allowBuilds:
1226
1322
  esbuild: true
1227
- msgpackr-extract: false
1323
+ msgpackr-extract: false${options.prisma
1324
+ ? `
1325
+ # The Prisma CLI's install scripts fetch its engines; pnpm 11 refuses to finish
1326
+ # an install (ERR_PNPM_IGNORED_BUILDS) while a dependency's build is unapproved.
1327
+ '@prisma/engines': true
1328
+ prisma: true`
1329
+ : ''}
1228
1330
  # @basaltkit/* releases in lockstep, often within hours — exclude the scope (and
1229
1331
  # create-basalt, which serves \`pnpm basalt update\`) from pnpm's minimumReleaseAge
1230
1332
  # policy so an update is never blocked on a fresh release.
package/dist/versions.js CHANGED
@@ -1,29 +1,31 @@
1
1
  // GENERATED by scripts/sync-versions.mjs (runs before every build) — do not edit.
2
2
  // The dependency range a new app gets for each @basaltkit/* package: the
3
- // current release line (^<major>.<minor>.0) of the workspace version.
3
+ // workspace version as the floor (^<major>.<minor>.<patch>), so a new app never
4
+ // resolves an older patch of the same line.
4
5
  export const SCAFFOLD_VERSIONS = {
5
6
  '@basaltkit/admin': '^2.0.0',
6
- '@basaltkit/admin-shadcn': '^1.1.0',
7
- '@basaltkit/ai-mcp': '^0.3.0',
8
- '@basaltkit/auth': '^4.1.0',
9
- '@basaltkit/auth-prisma': '^2.0.0',
10
- '@basaltkit/cli': '^1.3.0',
11
- '@basaltkit/config': '^1.1.0',
12
- '@basaltkit/core': '^1.5.0',
13
- '@basaltkit/env': '^3.1.0',
7
+ '@basaltkit/admin-shadcn': '^1.1.1',
8
+ '@basaltkit/ai-mcp': '^0.4.0',
9
+ '@basaltkit/auth': '^5.0.0',
10
+ '@basaltkit/auth-prisma': '^3.0.0',
11
+ '@basaltkit/cli': '^1.4.1',
12
+ '@basaltkit/config': '^1.1.2',
13
+ '@basaltkit/core': '^1.6.0',
14
+ '@basaltkit/env': '^3.1.1',
14
15
  '@basaltkit/events': '^1.3.0',
15
- '@basaltkit/fastify': '^2.5.0',
16
- '@basaltkit/generator': '^1.5.0',
16
+ '@basaltkit/fastify': '^3.0.0',
17
+ '@basaltkit/generator': '^1.6.0',
17
18
  '@basaltkit/logger': '^1.3.0',
18
- '@basaltkit/mcp': '^5.0.0',
19
- '@basaltkit/prisma': '^3.0.0',
19
+ '@basaltkit/mailer': '^2.1.0',
20
+ '@basaltkit/mcp': '^6.0.0',
21
+ '@basaltkit/prisma': '^4.0.0',
20
22
  '@basaltkit/sdk': '^2.1.0',
21
- '@basaltkit/subscriptions': '^5.0.0',
22
- '@basaltkit/subscriptions-prisma': '^3.0.0',
23
- '@basaltkit/teams': '^4.0.0',
23
+ '@basaltkit/subscriptions': '^6.0.0',
24
+ '@basaltkit/subscriptions-prisma': '^4.0.0',
25
+ '@basaltkit/teams': '^4.1.1',
24
26
  '@basaltkit/teams-prisma': '^1.1.0',
25
- '@basaltkit/tenancy': '^3.1.0',
26
- '@basaltkit/tenancy-prisma': '^2.0.0',
27
- '@basaltkit/testing': '^2.0.0',
28
- 'create-basalt': '^1.11.0',
27
+ '@basaltkit/tenancy': '^4.0.0',
28
+ '@basaltkit/tenancy-prisma': '^3.0.0',
29
+ '@basaltkit/testing': '^3.0.0',
30
+ 'create-basalt': '^1.13.0',
29
31
  };