create-stitchkit 0.3.3 → 0.4.1

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.
Files changed (99) hide show
  1. package/CHANGELOG.md +347 -0
  2. package/README.md +3 -1
  3. package/UPGRADING.md +342 -0
  4. package/dist/cli.js +238 -42
  5. package/examples/repository/_env.example.append +21 -0
  6. package/examples/repository/packages/backend/src/domain/repository/github-cache.ts +2 -2
  7. package/examples/repository/packages/backend/src/surface.ts +1 -1
  8. package/examples/repository/packages/config/src/features.ts +17 -0
  9. package/examples/repository/packages/frontend/src/app/[locale]/page.tsx +4 -4
  10. package/examples/repository/packages/frontend/src/app/[locale]/starter-page.tsx +2 -2
  11. package/examples/repository/packages/frontend/src/app/api/[...path]/route.ts +66 -0
  12. package/examples/repository/packages/frontend/src/lib/api/client.ts +17 -12
  13. package/examples/repository/packages/frontend/src/lib/api/cross-origin.ts +87 -0
  14. package/examples/repository/packages/frontend/src/lib/api/place.ts +26 -0
  15. package/examples/repository/packages/frontend/src/lib/api/queries.ts +3 -0
  16. package/examples/repository/packages/frontend/src/lib/api/server-client.ts +11 -0
  17. package/examples/repository/packages/frontend/src/lib/realtime/repository.ts +44 -12
  18. package/examples/repository/packages/frontend/src/providers/client-providers.tsx +30 -0
  19. package/examples/repository/packages/frontend/src/providers/index.tsx +17 -12
  20. package/examples/repository/packages/frontend/src/providers/realtime.tsx +6 -4
  21. package/examples/repository/project.json +189 -0
  22. package/examples/repository/scripts/runtime-smoke.ts +38 -6
  23. package/package.json +12 -2
  24. package/template/AGENTS.md +23 -3
  25. package/template/README.md +83 -8
  26. package/template/_env.example +16 -4
  27. package/template/_gitignore +1 -0
  28. package/template/biome.json +6 -2
  29. package/template/bun.lock +115 -98
  30. package/template/e2e/starter.spec.ts +5 -7
  31. package/template/ecosystem.config.cjs +42 -19
  32. package/template/ecosystem.dev.config.cjs +41 -21
  33. package/template/package.json +12 -10
  34. package/template/packages/backend/package.json +2 -2
  35. package/template/packages/backend/src/cleanup.ts +121 -0
  36. package/template/packages/backend/src/cli.ts +6 -2
  37. package/template/packages/backend/src/index.ts +33 -8
  38. package/template/packages/backend/src/surface.ts +6 -1
  39. package/template/packages/backend/src/transport/errors.ts +4 -2
  40. package/template/packages/config/package.json +6 -2
  41. package/template/packages/config/src/app-identity.generated.ts +20 -0
  42. package/template/packages/config/src/declaration.ts +30 -0
  43. package/template/packages/config/src/server.ts +8 -17
  44. package/template/packages/config/src/shutdown.ts +20 -0
  45. package/template/packages/config/src/variables.ts +89 -0
  46. package/template/packages/db/package.json +2 -2
  47. package/template/packages/frontend/next.config.ts +3 -2
  48. package/template/packages/frontend/package.json +13 -13
  49. package/template/packages/frontend/scripts/serve.ts +70 -0
  50. package/template/packages/frontend/src/app/[locale]/layout.tsx +9 -8
  51. package/template/packages/frontend/src/app/[locale]/page.tsx +2 -2
  52. package/template/packages/frontend/src/app/[locale]/starter-page.tsx +2 -2
  53. package/template/packages/frontend/src/app/[locale]/ui/[story]/page.tsx +1 -1
  54. package/template/packages/frontend/src/app/[locale]/ui/_catalogue/landing-showcase.tsx +1 -1
  55. package/template/packages/frontend/src/app/robots.ts +4 -2
  56. package/template/packages/frontend/src/app/sitemap.ts +7 -19
  57. package/template/packages/frontend/src/components/ui/toaster.tsx +4 -1
  58. package/template/packages/frontend/src/env.ts +27 -8
  59. package/template/packages/frontend/src/lib/seo/cache-by-origin.test.ts +68 -0
  60. package/template/packages/frontend/src/lib/seo/cache-by-origin.ts +40 -0
  61. package/template/packages/frontend/src/lib/seo/metadata.ts +68 -11
  62. package/template/packages/frontend/src/lib/seo/pages.ts +1 -1
  63. package/template/packages/frontend/src/lib/seo/request-origin.ts +89 -0
  64. package/template/packages/frontend/src/theme/config.ts +1 -1
  65. package/template/packages/frontend/tsconfig.json +10 -3
  66. package/template/packages/shared/package.json +1 -1
  67. package/template/playwright.config.ts +1 -1
  68. package/template/project.json +169 -0
  69. package/template/scripts/acceptance-database.test.ts +73 -0
  70. package/template/scripts/acceptance-database.ts +92 -0
  71. package/template/scripts/acceptance-local.ts +144 -0
  72. package/template/scripts/build-inputs.test.ts +69 -0
  73. package/template/scripts/build-inputs.ts +58 -0
  74. package/template/scripts/build-stamp.test.ts +151 -0
  75. package/template/scripts/build-stamp.ts +169 -0
  76. package/template/scripts/check-authored.ts +18 -2
  77. package/template/scripts/client-boundary.test.ts +117 -0
  78. package/template/scripts/client-boundary.ts +148 -0
  79. package/template/scripts/declaration.test.ts +206 -0
  80. package/template/scripts/declaration.ts +271 -0
  81. package/template/scripts/deployment-preflight.ts +41 -0
  82. package/template/scripts/dev.ts +43 -20
  83. package/template/scripts/local-env.test.ts +2 -2
  84. package/template/scripts/local-env.ts +9 -3
  85. package/template/scripts/readiness.ts +92 -0
  86. package/template/scripts/release-steps.test.ts +87 -0
  87. package/template/scripts/release-steps.ts +112 -0
  88. package/template/scripts/release.ts +38 -0
  89. package/template/scripts/runtime-smoke.test.ts +178 -0
  90. package/template/scripts/runtime-smoke.ts +21 -5
  91. package/template/scripts/serve-mode.test.ts +36 -0
  92. package/template/scripts/shutdown-budget.fixture.ts +29 -0
  93. package/template/scripts/shutdown-budget.test.ts +164 -0
  94. package/template/scripts/supervision-signal.test.ts +94 -0
  95. package/template/scripts/surface-conformance.ts +8 -1
  96. package/template/scripts/tooling-env.ts +35 -3
  97. package/template/scripts/web-surface-smoke.ts +183 -2
  98. package/template/app.config.json +0 -9
  99. package/template/packages/config/src/identity.ts +0 -18
@@ -0,0 +1,189 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "kind": "application",
4
+ "identity": {
5
+ "slug": "stitchkit-starter",
6
+ "name": "Stitchkit Starter",
7
+ "version": "0.1.0",
8
+ "description": {
9
+ "en": "Stitchkit Starter is a production application built with Stitchkit.",
10
+ "ru": "Stitchkit Starter — production-приложение на Stitchkit."
11
+ }
12
+ },
13
+ "roles": [
14
+ {
15
+ "name": "api",
16
+ "workingDirectory": "packages/backend",
17
+ "commands": {
18
+ "development": {
19
+ "executable": "bun",
20
+ "args": [
21
+ "--watch",
22
+ "src/index.ts"
23
+ ]
24
+ },
25
+ "production": {
26
+ "executable": "bun",
27
+ "args": [
28
+ "dist/index.js"
29
+ ]
30
+ }
31
+ },
32
+ "listener": {
33
+ "portVariable": "API_PORT",
34
+ "bindVariable": "BIND_HOST",
35
+ "readinessPath": "/health"
36
+ },
37
+ "drainFloorMs": 15000
38
+ },
39
+ {
40
+ "name": "web",
41
+ "workingDirectory": "packages/frontend",
42
+ "commands": {
43
+ "development": {
44
+ "executable": "bun",
45
+ "args": [
46
+ "scripts/serve.ts",
47
+ "development"
48
+ ]
49
+ },
50
+ "production": {
51
+ "executable": "bun",
52
+ "args": [
53
+ "scripts/serve.ts",
54
+ "production"
55
+ ]
56
+ }
57
+ },
58
+ "listener": {
59
+ "portVariable": "WEB_PORT",
60
+ "bindVariable": "BIND_HOST",
61
+ "readinessPath": "/"
62
+ },
63
+ "drainFloorMs": 5000
64
+ }
65
+ ],
66
+ "build": {
67
+ "command": {
68
+ "executable": "bun",
69
+ "args": [
70
+ "run",
71
+ "build"
72
+ ]
73
+ },
74
+ "artifacts": [
75
+ "packages/backend/dist",
76
+ "packages/frontend/.next",
77
+ "packages/db/src/generated"
78
+ ]
79
+ },
80
+ "requires": [
81
+ {
82
+ "name": "postgres",
83
+ "phases": [
84
+ "release",
85
+ "start"
86
+ ]
87
+ }
88
+ ],
89
+ "release": {
90
+ "migrations": {
91
+ "engine": "prisma",
92
+ "root": "packages/db/migrations",
93
+ "lockfile": "packages/db/migrations/migration_lock.toml"
94
+ }
95
+ },
96
+ "env": {
97
+ "variables": [
98
+ {
99
+ "name": "API_PORT",
100
+ "shape": "integer",
101
+ "required": true
102
+ },
103
+ {
104
+ "name": "BIND_HOST",
105
+ "shape": "string",
106
+ "required": false
107
+ },
108
+ {
109
+ "name": "CORS_ORIGIN",
110
+ "shape": "url",
111
+ "required": false
112
+ },
113
+ {
114
+ "name": "DATABASE_URL",
115
+ "shape": "url",
116
+ "required": true
117
+ },
118
+ {
119
+ "name": "GITHUB_API_URL",
120
+ "shape": "url",
121
+ "required": false
122
+ },
123
+ {
124
+ "name": "GITHUB_CACHE_TTL_SECONDS",
125
+ "shape": "integer",
126
+ "required": false
127
+ },
128
+ {
129
+ "name": "GITHUB_REPOSITORY",
130
+ "shape": "string",
131
+ "required": true
132
+ },
133
+ {
134
+ "name": "GITHUB_TOKEN",
135
+ "shape": "string",
136
+ "required": false
137
+ },
138
+ {
139
+ "name": "INTERNAL_API_URL",
140
+ "shape": "url",
141
+ "required": true
142
+ },
143
+ {
144
+ "name": "LOG_FORMAT",
145
+ "shape": "enum",
146
+ "required": false,
147
+ "members": [
148
+ "pretty",
149
+ "json"
150
+ ]
151
+ },
152
+ {
153
+ "name": "NODE_ENV",
154
+ "shape": "enum",
155
+ "required": false,
156
+ "members": [
157
+ "development",
158
+ "test",
159
+ "production"
160
+ ]
161
+ },
162
+ {
163
+ "name": "PUBLIC_API_ORIGIN",
164
+ "shape": "url",
165
+ "required": false
166
+ },
167
+ {
168
+ "name": "PUBLIC_REALTIME_ORIGIN",
169
+ "shape": "url",
170
+ "required": false
171
+ },
172
+ {
173
+ "name": "PUBLIC_WEB_HOSTS",
174
+ "shape": "string",
175
+ "required": false
176
+ },
177
+ {
178
+ "name": "PUBLIC_WEB_ORIGIN",
179
+ "shape": "url",
180
+ "required": false
181
+ },
182
+ {
183
+ "name": "WEB_PORT",
184
+ "shape": "integer",
185
+ "required": true
186
+ }
187
+ ]
188
+ }
189
+ }
@@ -1,15 +1,24 @@
1
1
  import { RepositorySnapshotSchema, repositoryRealtimeContract } from '@app/shared';
2
2
  import { createRealtimeClient, defineRealtimeContract } from 'stitchkit';
3
3
  import { z } from 'zod';
4
+ import { assertDeploymentIsAnswering } from './deployment-preflight';
4
5
  import { defineSurfaceProbe, runSurfaceConformance } from './surface-conformance';
5
6
  import { loadToolingEnv } from './tooling-env';
6
- import { assertPublicWebSurface } from './web-surface-smoke';
7
+ import { assertArtifactIsPlacementFree, assertPublicWebSurface } from './web-surface-smoke';
7
8
 
8
9
  const toolingEnv = loadToolingEnv();
9
- const apiOrigin = toolingEnv.NEXT_PUBLIC_API_URL;
10
+ const apiOrigin = toolingEnv.SMOKE_API_ORIGIN;
11
+
12
+ await assertDeploymentIsAnswering({
13
+ 'the API role': apiOrigin,
14
+ 'the web role': toolingEnv.SMOKE_WEB_ORIGIN,
15
+ });
10
16
 
11
17
  async function json(path: string, init?: RequestInit): Promise<unknown> {
12
- const response = await fetch(`${apiOrigin}${path}`, init);
18
+ const response = await fetch(`${apiOrigin}${path}`, {
19
+ ...init,
20
+ signal: AbortSignal.timeout(30_000),
21
+ });
13
22
  if (!response.ok)
14
23
  throw new Error(`${init?.method ?? 'GET'} ${path} returned ${response.status}`);
15
24
  return response.json();
@@ -155,7 +164,7 @@ await runSurfaceConformance({
155
164
  });
156
165
  if (
157
166
  corsResponse.headers.get('access-control-allow-origin') !==
158
- new URL(toolingEnv.NEXT_PUBLIC_WEB_URL).origin
167
+ new URL(toolingEnv.SMOKE_WEB_ORIGIN).origin
159
168
  ) {
160
169
  throw new Error('API CORS origin differs from the configured web origin');
161
170
  }
@@ -167,6 +176,29 @@ await runSurfaceConformance({
167
176
  }
168
177
  }
169
178
 
170
- await assertPublicWebSurface(toolingEnv.NEXT_PUBLIC_WEB_URL);
179
+ {
180
+ // THE DEFAULT PATH, end to end: the browser's own origin answers `/api/…`
181
+ // because the web role forwards it. This is what makes the example's client a
182
+ // module constant, so it is checked rather than described.
183
+ const proxied = await fetch(`${toolingEnv.SMOKE_WEB_ORIGIN}/api/repository`);
184
+ if (!proxied.ok) {
185
+ throw new Error(
186
+ `The web role did not forward /api/repository (${proxied.status}) — the same-origin default is broken`,
187
+ );
188
+ }
189
+ const sameOrigin = RepositorySnapshotSchema.parse(await proxied.json());
190
+ const direct = RepositorySnapshotSchema.parse(await json('/api/repository/'));
191
+ if (sameOrigin.fullName !== direct.fullName) {
192
+ throw new Error("the forwarded answer differs from the API role's own");
193
+ }
194
+ }
195
+
196
+ await assertPublicWebSurface(toolingEnv.SMOKE_WEB_ORIGIN);
197
+ await assertArtifactIsPlacementFree(toolingEnv.SMOKE_WEB_ORIGIN, {
198
+ origin: toolingEnv.PUBLIC_WEB_ORIGIN,
199
+ hosts: toolingEnv.PUBLIC_WEB_HOSTS,
200
+ });
171
201
 
172
- console.log('Runtime HTTP, OpenAPI, Socket.IO, MCP and public web smoke passed');
202
+ console.log(
203
+ 'Runtime HTTP (same-origin and direct), OpenAPI, Socket.IO, MCP and public web smoke passed',
204
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-stitchkit",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "Create a production-shaped Stitchkit application",
5
5
  "license": "MIT",
6
6
  "author": "Max Listov <maxlistov@gmail.com>",
@@ -18,6 +18,7 @@
18
18
  "template/**/*",
19
19
  "examples/**/*",
20
20
  "!template/**/.env",
21
+ "!template/**/.build-stamp.json",
21
22
  "!template/**/node_modules/**",
22
23
  "!template/**/.next/**",
23
24
  "!template/**/dist/**",
@@ -27,13 +28,22 @@
27
28
  "!template/**/src/generated/**",
28
29
  "!template/**/*.log",
29
30
  "!template/**/*.tsbuildinfo",
31
+ "!template/**/coverage/**",
32
+ "!examples/**/.env",
33
+ "!examples/**/.build-stamp.json",
30
34
  "!examples/**/node_modules/**",
31
35
  "!examples/**/.next/**",
32
36
  "!examples/**/dist/**",
37
+ "!examples/**/coverage/**",
38
+ "!examples/**/playwright-report/**",
39
+ "!examples/**/test-results/**",
40
+ "!examples/**/next-env.d.ts",
41
+ "!examples/**/src/generated/**",
33
42
  "!examples/**/*.log",
34
43
  "!examples/**/*.tsbuildinfo",
35
44
  "README.md",
36
45
  "CHANGELOG.md",
46
+ "UPGRADING.md",
37
47
  "LICENSE"
38
48
  ],
39
49
  "publishConfig": {
@@ -49,7 +59,7 @@
49
59
  "zod": "^4.4.3"
50
60
  },
51
61
  "devDependencies": {
52
- "@types/bun": "^1.3.14",
62
+ "@types/bun": "^1.4.0",
53
63
  "typescript": "^7.0.2"
54
64
  },
55
65
  "engines": {
@@ -12,8 +12,19 @@ framework source repository.
12
12
  owns application policy. Routes remain thin transport boundaries.
13
13
  - `packages/frontend` owns Next.js pages, typed clients, query/mutation hooks and
14
14
  cache reactions.
15
- - `packages/config/src/server.ts` is the only server environment boundary.
16
- `app.config.json` is the only application identity boundary.
15
+ - `packages/config/src/variables.ts` is the only declaration of an environment
16
+ variable. `server.ts` and `frontend/src/env.ts` are projections of it, and
17
+ the declaration's `env.variables` is derived from it — never restated.
18
+ - `project.json` is the only place this project describes itself. It is written
19
+ by machine — the scaffolder stamps the identity, `bun run gen:declaration`
20
+ derives `env.variables` — so the formatter leaves it alone and
21
+ `scripts/declaration.test.ts` is what checks it.
22
+ - Four things are generated from it and must not be hand-edited:
23
+ `ecosystem.config.cjs`, `ecosystem.dev.config.cjs`,
24
+ `packages/config/src/app-identity.generated.ts` and the `env.variables` block
25
+ of `project.json`. Run `bun run gen:declaration` after changing a role. It
26
+ holds nothing that differs between two deployments; those are named there by
27
+ variable and supplied by the place.
17
28
 
18
29
  Dependencies point inward: frontend/backend → shared; backend → db/config.
19
30
  Shared never imports an application runtime package.
@@ -48,6 +59,15 @@ vertical path. Before handing work off, run:
48
59
  bun run check
49
60
  bun run test
50
61
  bun run build
51
- bun run runtime:smoke
62
+ bun run acceptance:local
52
63
  ```
53
64
 
65
+ `acceptance:local` is part of the list because `runtime:smoke` and `e2e` check a
66
+ running deployment: it creates one of its own — separate PM2 home, ephemeral
67
+ ports, and its own database from `ACCEPTANCE_DATABASE_URL` — runs both against
68
+ it, and destroys it. The separate database is not tidiness: the gates write, so
69
+ one borrowing `DATABASE_URL` writes rows wherever `.env` points. **Never put
70
+ `pm2:prod` in this list.** It applies the declared migrations to *your* database
71
+ and reloads the running deployment; deploying is its own command, asked for on
72
+ purpose, and no gate performs it.
73
+
@@ -2,9 +2,24 @@
2
2
 
3
3
  Production-shaped application generated by `create-stitchkit`.
4
4
 
5
- Application identity lives in [`app.config.json`](app.config.json). Change the
6
- slug, display name, version and localized description there; package names,
7
- process names, MCP/OpenAPI identity, UI copy and SEO derive from it.
5
+ This project describes itself in [`project.json`](project.json) — its
6
+ **declaration**: identity, the roles it runs, what it builds, what it needs
7
+ before it starts, and the environment variables a deployment must supply.
8
+
9
+ The declaration is true **with no machine in existence**. A field you cannot
10
+ fill in without knowing where the code will run is a *binding*, not a
11
+ declaration: ports, hosts, addresses, machine paths and supervision policy are
12
+ named there by variable and never by value. The schema has no field that asks
13
+ for one, so nothing in it ever requires a value of the place; where a value
14
+ could still be written into a free-text field, a filter refuses the known
15
+ shapes of a machine name. Change the slug, display name, version or description there and package
16
+ names, process names, MCP/OpenAPI identity, UI copy and SEO follow.
17
+
18
+ Four things are **generated** from it — `ecosystem.config.cjs`,
19
+ `ecosystem.dev.config.cjs`, `packages/config/src/app-identity.generated.ts` and
20
+ the `env.variables` block of the declaration itself. Run
21
+ `bun run gen:declaration` after changing a role; the test suite refuses a stale
22
+ copy.
8
23
 
9
24
  ## Start
10
25
 
@@ -35,6 +50,27 @@ Configure it with `GITHUB_REPOSITORY`; `GITHUB_TOKEN` is optional.
35
50
  Repository visibility then demonstrates the canonical Prisma enum → shared Zod schema
36
51
  → HTTP/UI path without duplicating its allowed values.
37
52
 
53
+ **How the browser reaches the API, and what each shape costs.** By default it
54
+ does not reach it at all: the browser calls its own origin (`/api/…`), the web
55
+ role forwards to the API role (`packages/frontend/src/app/api/[...path]/route.ts`),
56
+ and the client is a plain module constant — no address to wait for, no lazy
57
+ accessor, nothing to order relative to `<Providers>`. A single-origin deployment
58
+ supplies only `INTERNAL_API_URL`.
59
+
60
+ Two things can pull a deployment out of that, and they are separate questions,
61
+ so they have separate variables. **`PUBLIC_REALTIME_ORIGIN`** is the socket: a
62
+ WebSocket upgrade does not survive the route handler that forwards `/api`, so
63
+ two roles on two ports with nothing in front of them must name the socket's
64
+ origin even though their HTTP is already same-origin. Behind one routing layer
65
+ that forwards `/socket.io`, leave it unset. **`PUBLIC_API_ORIGIN`** is HTTP, for
66
+ a frontend that genuinely dials the API role itself — and setting it changes
67
+ nothing on its own: switching is one import in
68
+ `packages/frontend/src/lib/api/queries.ts`, documented in
69
+ `packages/frontend/src/lib/api/cross-origin.ts`. That variant costs a
70
+ server-delivered address, a client built on first use (hence the parentheses),
71
+ an ordering requirement relative to `<Providers>`, and `CORS_ORIGIN` on the API
72
+ role.
73
+
38
74
  The `/en/ui` catalogue is isolated under
39
75
  `packages/frontend/src/app/[locale]/ui` and can be removed as one directory;
40
76
  reusable primitives remain in `components/ui`. It renders the complete
@@ -54,22 +90,61 @@ Unsupported browsers and users requesting reduced motion switch immediately.
54
90
  bun run check
55
91
  bun run test
56
92
  bun run build
57
- bun run runtime:smoke
58
- bun run e2e
93
+ bun run acceptance:local
59
94
  ```
60
95
 
96
+ Top to bottom, in one terminal, and none of it touches a deployment. They
97
+ assume your development database is already set up — see [Start](#start).
98
+
99
+ `acceptance:local` is the last one because `runtime:smoke` and `e2e` check a
100
+ **running** deployment rather than a source tree — so it creates one and
101
+ destroys it: its own PM2 home, ephemeral ports, its own public-host allowlist,
102
+ and a stop that names the roles the declaration declares. It reloads nothing
103
+ you are running.
104
+
105
+ It also brings its own database, `ACCEPTANCE_DATABASE_URL`, and applies this
106
+ project's migrations to that — not to yours. The gates WRITE (the repository
107
+ example's smoke posts a refresh, and that upserts), so borrowing `DATABASE_URL`
108
+ would make a gate a writer in whatever database your `.env` names. It refuses to
109
+ start when the variable is unset or names the same database, and says what to
110
+ add. It needs `pm2` (see [Requirements](#requirements)).
111
+
112
+ Deploying is a separate, deliberate command — `bun run pm2:prod` under
113
+ [Production](#production) — and it is not a gate.
114
+
115
+ To run the two runtime gates against a deployment that already exists somewhere,
116
+ point `SMOKE_API_ORIGIN` / `SMOKE_WEB_ORIGIN` at it and call `bun run
117
+ runtime:smoke` / `bun run e2e` directly. `runtime:smoke` asks the web role to
118
+ answer as two of the addresses `PUBLIC_WEB_HOSTS` claims, to prove one artifact
119
+ serves many; a deployment claiming fewer than two addresses besides the one
120
+ being dialled is told exactly what to add rather than failing on a refused host.
121
+
122
+ ## Requirements
123
+
124
+ - **Bun** and **Node ≥ 22**.
125
+ - **PostgreSQL** — external infrastructure, in development and production
126
+ alike. This project owns its schema and migrations, never the database
127
+ process.
128
+ - **PM2** on `PATH` (`bun add --global pm2`) for `bun run dev`,
129
+ `bun run acceptance:local` and `bun run pm2:prod`.
130
+ - **Playwright browsers** (`bunx playwright install`) for `bun run e2e`.
131
+
61
132
  ## Production
62
133
 
63
134
  Provide a production `.env`, then:
64
135
 
65
136
  ```bash
66
- bun run db:deploy
67
137
  bun run build
68
138
  bun run pm2:prod
69
139
  ```
70
140
 
71
- The Next.js frontend and Stitchkit API are separate processes and can also be
72
- deployed independently.
141
+ `pm2:prod` is the release: it checks every artifact `build.artifacts` declares,
142
+ applies the migrations `release.migrations` declares, and only then starts the
143
+ roles from the generated supervision file. There is no separate `db:deploy`
144
+ step to remember — the declaration already says the migrations exist, and one
145
+ place saying it is the point.
146
+
147
+ The roles are separate processes and can also be deployed independently.
73
148
 
74
149
  PostgreSQL is external infrastructure in development and production. The
75
150
  application owns its schema and migrations; the environment owns the database
@@ -1,11 +1,23 @@
1
1
  NODE_ENV=development
2
2
  DATABASE_URL=postgresql://USER:PASSWORD@127.0.0.1:5432/stitchkit_starter
3
+ # The throwaway database `bun run acceptance:local` creates and writes to. The
4
+ # runtime gates WRITE, so they get one of their own: the harness refuses to
5
+ # start if this is unset or names the database above.
6
+ ACCEPTANCE_DATABASE_URL=postgresql://USER:PASSWORD@127.0.0.1:5432/stitchkit_starter_acceptance
3
7
  # 0.0.0.0 exposes the app to every network interface — opt in consciously.
4
8
  BIND_HOST=127.0.0.1
5
9
  API_PORT=3211
6
10
  WEB_PORT=3210
7
- NEXT_PUBLIC_API_URL=http://127.0.0.1:3211
8
- INTERNAL_API_URL=http://127.0.0.1:3211
9
- NEXT_PUBLIC_WEB_URL=http://127.0.0.1:3210
11
+ SMOKE_API_ORIGIN=http://127.0.0.1:3211
12
+ SMOKE_WEB_ORIGIN=http://127.0.0.1:3210
13
+ # Hosts this deployment answers for, comma-separated. One built artifact can
14
+ # serve several addresses; a forwarded host outside this list is refused rather
15
+ # than believed. Leave unset and set PUBLIC_WEB_ORIGIN instead for a single one.
16
+ #
17
+ # `bun run acceptance:local` supplies its own list to the deployment it creates,
18
+ # so the addresses the portability check needs are not policy this project
19
+ # carries. List here only the hosts this deployment really answers for.
20
+ PUBLIC_WEB_HOSTS=127.0.0.1:3210
10
21
  LOG_FORMAT=pretty
11
- CORS_ORIGIN=http://127.0.0.1:3210
22
+ # CORS_ORIGIN is only needed for a genuinely cross-origin browser.
23
+ # CORS_ORIGIN=http://127.0.0.1:3210
@@ -3,6 +3,7 @@ dist/
3
3
  .next/
4
4
  next-env.d.ts
5
5
  .env
6
+ .build-stamp.json
6
7
  coverage/
7
8
  *.log
8
9
  packages/db/src/generated/
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.5.7/schema.json",
2
+ "$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
3
3
  "files": {
4
4
  "includes": [
5
5
  "**",
@@ -9,7 +9,11 @@
9
9
  "!!playwright-report",
10
10
  "!!test-results",
11
11
  "!!packages/frontend/next-env.d.ts",
12
- "!!packages/frontend/public"
12
+ "!!packages/frontend/public",
13
+ "!!project.json",
14
+ "!!ecosystem.config.cjs",
15
+ "!!ecosystem.dev.config.cjs",
16
+ "!!packages/config/src/app-identity.generated.ts"
13
17
  ]
14
18
  },
15
19
  "formatter": {