create-stitchkit 0.5.1 → 0.6.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 (38) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/UPGRADING.md +66 -0
  3. package/examples/repository/packages/backend/src/surface.snapshot.json +32 -0
  4. package/examples/repository/packages/backend/src/surface.ts +49 -2
  5. package/examples/repository/packages/frontend/src/app/[locale]/starter-page.tsx +7 -0
  6. package/examples/repository/packages/shared/src/index.ts +3 -0
  7. package/examples/repository/project.json +10 -0
  8. package/package.json +10 -10
  9. package/template/.data/board.sqlite +0 -0
  10. package/template/biome.json +1 -1
  11. package/template/bun.lock +194 -179
  12. package/template/e2e/starter.spec.ts +7 -1
  13. package/template/package.json +14 -8
  14. package/template/packages/backend/package.json +5 -5
  15. package/template/packages/backend/src/index.ts +10 -2
  16. package/template/packages/backend/src/lib/board.ts +99 -0
  17. package/template/packages/backend/src/lib/live.ts +73 -0
  18. package/template/packages/backend/src/surface.snapshot.json +32 -0
  19. package/template/packages/backend/src/surface.ts +53 -3
  20. package/template/packages/backend/src/transport/board-service.ts +18 -0
  21. package/template/packages/config/package.json +3 -3
  22. package/template/packages/config/src/variables.ts +16 -0
  23. package/template/packages/db/package.json +3 -3
  24. package/template/packages/frontend/package.json +15 -15
  25. package/template/packages/frontend/src/app/[locale]/page.tsx +2 -0
  26. package/template/packages/frontend/src/app/[locale]/starter-page.tsx +9 -1
  27. package/template/packages/frontend/src/features/board/board-live.ts +91 -0
  28. package/template/packages/frontend/src/features/board/board-panel.tsx +112 -0
  29. package/template/packages/frontend/src/lib/query-client.test.ts +6 -0
  30. package/template/packages/frontend/src/lib/query-client.ts +25 -17
  31. package/template/packages/shared/package.json +2 -2
  32. package/template/packages/shared/src/contracts/board.ts +54 -0
  33. package/template/packages/shared/src/contracts/live.ts +20 -0
  34. package/template/packages/shared/src/index.ts +3 -0
  35. package/template/packages/shared/src/schemas/board.ts +41 -0
  36. package/template/project.json +10 -0
  37. package/templates/agent/biome.json +1 -1
  38. package/templates/agent/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -12,6 +12,62 @@ step is overwritten by the next release.
12
12
 
13
13
  ## [Unreleased]
14
14
 
15
+ ## [0.6.1] — 2026-09-20
16
+
17
+ ### Changed
18
+
19
+ - **The generated frontend carries the canonical React Query runtime shape.**
20
+ `lib/query-client.ts` owns request-local SSR identity, one browser singleton,
21
+ pending dehydration, application `staleTime` and the retry policy a released
22
+ core already lets it state — one retry for queries, none for mutations — with
23
+ a test that keeps it there. It does not depend on an unreleased Stitchkit
24
+ export; `UPGRADING.md` records the one-step cutover to
25
+ `createQueryClientFactory` after the matching core release is available.
26
+ - **The generated workspace now targets Stitchkit `^0.90.5` and current stable
27
+ dependencies.** Its root and agent lockfiles were regenerated independently while
28
+ preserving the single `catalog.stitchkit` source and workspace `catalog:` links.
29
+
30
+ ## [0.6.0] — 2026-09-02
31
+
32
+ The generated project stops being an empty frame with a to-do at the bottom of
33
+ the page. It ships **one vertical feature**, from schema to transport to UI —
34
+ and it was chosen so that every live primitive Stitchkit gained in 0.75–0.76 is
35
+ there because the feature needs it, not to demonstrate anything.
36
+
37
+ The demonstration is the second browser tab: post from it, and the first one
38
+ updates without asking. Nothing on that page polls, and nothing refetches after
39
+ a write.
40
+
41
+ ### Added
42
+
43
+ - **A live board.** `board.list` is a watched read and `board.post` writes to it.
44
+ Together they use, and only where they are needed:
45
+ - `defineEvents` — one topic, `board.changed`, declared in `shared` because the
46
+ server announces it and the browser subscribes to it;
47
+ - `defineKeyspace` / `openKeyspace` over SQLite — notes are authoritative in
48
+ memory and durable behind it, so a read in a handler needs no `await` and a
49
+ restart loses nothing;
50
+ - `createWatchHub` / `createWatchClient` — every browser asking the same
51
+ question is **one read** on the server, re-run when the topic says the answer
52
+ may have changed;
53
+ - `createTrustFence` — installed on **both** lanes when `TRUSTED_HOSTS` is set,
54
+ because the Socket.IO lane never reaches a lifecycle hook.
55
+
56
+ The comments say which of these to reach for and, more usefully, when not to:
57
+ a keyspace is for a small bounded set the process wants synchronously, and the
58
+ moment a thing wants queries, relations or unbounded growth it is a database
59
+ row and Prisma is already there for it.
60
+
61
+ - **Two environment variables**, both declared in the one place the project
62
+ declares variables: `BOARD_STORE_PATH` (defaulted, and the directory is created
63
+ by the application rather than by whoever deploys it) and `TRUSTED_HOSTS`
64
+ (unset means no fence, which is honest for a laptop and wrong for anything a
65
+ network can reach — a fence cannot invent the names it should answer to).
66
+
67
+ ### Changed
68
+
69
+ - The template now targets `stitchkit` `^0.76.1`, up from `^0.71.0`.
70
+
15
71
  ## [0.5.1] — 2026-09-01
16
72
 
17
73
  Three findings from someone setting up a new application on the starter from
package/UPGRADING.md CHANGED
@@ -60,6 +60,72 @@ the first scaffolder release with a migration channel of its own.
60
60
 
61
61
  ---
62
62
 
63
+ ## Released migration: 0.6.1
64
+
65
+ ### Canonical query client factory
66
+
67
+ This migration is additive, but it has a dependency order: first upgrade to a
68
+ Stitchkit release that exports `createQueryClientFactory` from
69
+ `stitchkit/react`. Then replace the local QueryClient construction with:
70
+
71
+ ```ts
72
+ import { cache } from 'react';
73
+ import { createQueryClientFactory } from 'stitchkit/react';
74
+
75
+ export const getQueryClient = createQueryClientFactory({
76
+ serverCache: cache,
77
+ queryClient: {
78
+ defaultOptions: { queries: { staleTime: 30_000 } },
79
+ },
80
+ });
81
+ ```
82
+
83
+ Keep project-specific mutation toasts or cache configuration in the factory
84
+ options. Do not copy the framework retry predicate back into the application.
85
+
86
+ ## Released migration: 0.6.0
87
+
88
+ The scaffolder gained a vertical feature. Adopting it in a project you already
89
+ own is optional — nothing breaks if you skip it — but two things are **operator
90
+ steps**, and skipping those with the feature adopted means the API will not
91
+ start.
92
+
93
+ ### 1. The store directory has to be writable
94
+
95
+ `BOARD_STORE_PATH` defaults to `.data/board.sqlite`, relative to the API role's
96
+ working directory. The application creates the directory itself; what it cannot
97
+ do is make a read-only volume writable.
98
+
99
+ ```bash
100
+ # on the machine, as the user the API runs as
101
+ test -w "$(dirname "${BOARD_STORE_PATH:-.data/board.sqlite}")" || echo "not writable"
102
+ ```
103
+
104
+ If the role runs from a read-only image, point `BOARD_STORE_PATH` at a mounted
105
+ volume instead.
106
+
107
+ ### 2. Decide about the trust fence, on purpose
108
+
109
+ `TRUSTED_HOSTS` is unset by default, and unset means **no fence**. That is
110
+ correct on a laptop and wrong on anything a network reaches — but a fence cannot
111
+ guess which names your deployment answers to, so it refuses to invent them.
112
+
113
+ ```bash
114
+ # every authority this deployment answers on, comma separated
115
+ TRUSTED_HOSTS=app.internal,app.internal:5181
116
+ ```
117
+
118
+ Set it and the fence is installed on both lanes: HTTP before routing, and the
119
+ realtime handshake, which never reaches a lifecycle hook on either runtime. If
120
+ your browser lives on another origin you already declared it as `CORS_ORIGIN`,
121
+ and the fence reads that one rather than asking you a second time.
122
+
123
+ ### 3. Nothing else
124
+
125
+ The rest of the feature is code you either copy or do not. The framework range
126
+ moved to `^0.76.1`; if you upgrade the dependency without taking the feature,
127
+ [the framework's own guide](../../docs/guide/upgrading.md) is the one to follow.
128
+
63
129
  ## Released migration: 0.5.0
64
130
 
65
131
  ### the approval policy names a tool that no longer exists
@@ -1,4 +1,36 @@
1
1
  [
2
+ {
3
+ "service": "board",
4
+ "action": "list",
5
+ "scope": "public",
6
+ "hasInput": false,
7
+ "hasOutput": true,
8
+ "inputShape": null,
9
+ "outputShape": "ce49fe198eb3412f",
10
+ "http": [
11
+ {
12
+ "method": "GET",
13
+ "path": "/api/board"
14
+ }
15
+ ],
16
+ "tools": {}
17
+ },
18
+ {
19
+ "service": "board",
20
+ "action": "post",
21
+ "scope": "public",
22
+ "hasInput": true,
23
+ "hasOutput": true,
24
+ "inputShape": "b1d8eb61c62cafed",
25
+ "outputShape": "ce49fe198eb3412f",
26
+ "http": [
27
+ {
28
+ "method": "POST",
29
+ "path": "/api/board"
30
+ }
31
+ ],
32
+ "tools": {}
33
+ },
2
34
  {
3
35
  "service": "repository",
4
36
  "action": "read",
@@ -1,16 +1,63 @@
1
1
  import { env } from '@app/config';
2
2
  import { repositoryRealtimeContract } from '@app/shared';
3
- import { bindRealtimeServer, createSocketIOServer } from 'stitchkit/server';
3
+ import type { TrustFence } from 'stitchkit/server';
4
+ import { bindRealtimeServer, createSocketIOServer, createTrustFence } from 'stitchkit/server';
5
+ import { type BoardRuntime, openBoard } from './lib/board';
6
+ import { bindLive } from './lib/live';
7
+ import { createBoardService } from './transport/board-service';
4
8
  import { createRepositoryService } from './transport/repository-service';
5
9
  import { createSystemService } from './transport/system-service';
6
10
 
11
+ /**
12
+ * The fence this deployment answers behind, or nothing.
13
+ *
14
+ * `trustedHosts` has no default and cannot have one: a fence compares the
15
+ * authority a request addressed against a list, and it cannot invent the list.
16
+ * Unset means no fence — honest for a checkout on a laptop, wrong for anything
17
+ * a network can reach.
18
+ */
19
+ function createFence(): TrustFence | undefined {
20
+ if (!env.TRUSTED_HOSTS) return undefined;
21
+ const browserOrigin = env.CORS_ORIGIN ? [new URL(env.CORS_ORIGIN).host] : [];
22
+ return createTrustFence({
23
+ trustedHosts: env.TRUSTED_HOSTS.split(',').map((entry) => entry.trim()),
24
+ ...(browserOrigin.length > 0 && { trustedOrigins: browserOrigin }),
25
+ onRefused: (refusal) => {
26
+ console.warn(
27
+ `Refused a ${refusal.lane} request: ${refusal.reason} (host ${refusal.host ?? 'absent'})`,
28
+ );
29
+ },
30
+ });
31
+ }
32
+
7
33
  export async function createSurface() {
34
+ const board: BoardRuntime = await openBoard();
35
+ const fence = createFence();
36
+
8
37
  const socket = await createSocketIOServer({
9
38
  cors: { origin: env.CORS_ORIGIN ?? [] },
39
+ // The socket's own admission point. `/socket.io/*` never reaches a lifecycle
40
+ // hook on either runtime, so a fence installed only in `hooks` would leave
41
+ // open the lane this app pushes its live data over.
42
+ ...(fence && { allowRequest: fence.allowRequest }),
10
43
  });
44
+
45
+ // Two bindings over one socket, deliberately. This example's own contract and
46
+ // the live one are separate declarations with separate event names, and
47
+ // merging them into a single registry here would mean editing the merge every
48
+ // time either side gains a topic.
11
49
  const realtime = bindRealtimeServer(repositoryRealtimeContract, socket);
50
+ const live = bindLive(board, socket);
51
+
12
52
  const repositoryService = createRepositoryService((snapshot) =>
13
53
  realtime.emit('repository:refreshed', snapshot),
14
54
  );
15
- return { socket, services: [createSystemService(), repositoryService] };
55
+
56
+ return {
57
+ socket,
58
+ fence,
59
+ board,
60
+ hub: live.hub,
61
+ services: [createSystemService(), createBoardService(board), repositoryService],
62
+ };
16
63
  }
@@ -4,6 +4,7 @@ import { BrandMark } from '@/components/brand-mark';
4
4
  import { RepositorySummary } from '@/components/repository-summary';
5
5
  import { LanguageSwitcher, ThemeToggle } from '@/components/system-controls';
6
6
  import { buttonVariants } from '@/components/ui';
7
+ import { BoardPanel } from '@/features/board/board-panel';
7
8
  import type { AppLocale } from '@/i18n/locales';
8
9
  import { Link } from '@/i18n/navigation';
9
10
  import { absoluteSiteUrl } from '@/lib/seo/metadata';
@@ -15,6 +16,7 @@ interface StarterPageProps {
15
16
  applicationDescription: string;
16
17
  heroTitle: string;
17
18
  catalogueLabel: string;
19
+ realtimeOrigin?: string;
18
20
  locale: AppLocale;
19
21
  }
20
22
 
@@ -62,6 +64,7 @@ export async function StarterPage({
62
64
  applicationDescription,
63
65
  heroTitle,
64
66
  catalogueLabel,
67
+ realtimeOrigin,
65
68
  locale,
66
69
  }: StarterPageProps) {
67
70
  const homeSeo = getSeoPage('home', locale);
@@ -140,6 +143,10 @@ export async function StarterPage({
140
143
  </div>
141
144
  </div>
142
145
 
146
+ <div className='mt-6 w-full max-w-xl'>
147
+ <BoardPanel realtimeOrigin={realtimeOrigin} />
148
+ </div>
149
+
143
150
  <div className='mt-5 w-full'>
144
151
  <RepositorySummary />
145
152
  </div>
@@ -1,5 +1,8 @@
1
+ export * from './contracts/board';
2
+ export * from './contracts/live';
1
3
  export * from './contracts/repository';
2
4
  export * from './contracts/system';
3
5
  export * from './realtime/repository';
6
+ export * from './schemas/board';
4
7
  export * from './schemas/repository';
5
8
  export * from './schemas/system';
@@ -105,6 +105,11 @@
105
105
  "shape": "string",
106
106
  "required": false
107
107
  },
108
+ {
109
+ "name": "BOARD_STORE_PATH",
110
+ "shape": "string",
111
+ "required": false
112
+ },
108
113
  {
109
114
  "name": "CORS_ORIGIN",
110
115
  "shape": "url",
@@ -179,6 +184,11 @@
179
184
  "shape": "url",
180
185
  "required": false
181
186
  },
187
+ {
188
+ "name": "TRUSTED_HOSTS",
189
+ "shape": "string",
190
+ "required": false
191
+ },
182
192
  {
183
193
  "name": "WEB_PORT",
184
194
  "shape": "integer",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-stitchkit",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "Create a production-shaped Stitchkit application",
5
5
  "license": "MIT",
6
6
  "author": "Max Listov <maxlistov@gmail.com>",
@@ -73,17 +73,17 @@
73
73
  "prepublishOnly": "bun run check && bun run test && bun run build"
74
74
  },
75
75
  "dependencies": {
76
- "zod": "^4.4.3"
76
+ "zod": "^4.6.5"
77
77
  },
78
78
  "devDependencies": {
79
- "@opentui/core": "^0.5.9",
80
- "@opentui/react": "^0.5.9",
81
- "@openrouter/ai-sdk-provider": "^3.0.0",
82
- "@types/bun": "^1.4.0",
83
- "@types/react": "^19.2.18",
84
- "ai": "^7.0.84",
85
- "react": "^19.2.8",
86
- "stitchkit": "0.71.0",
79
+ "@opentui/core": "^0.5.11",
80
+ "@opentui/react": "^0.5.11",
81
+ "@openrouter/ai-sdk-provider": "^3.1.0",
82
+ "@types/bun": "^1.4.2",
83
+ "@types/react": "^19.3.0",
84
+ "ai": "^7.0.107",
85
+ "react": "^19.3.0",
86
+ "stitchkit": "0.90.5",
87
87
  "typescript": "^7.0.2"
88
88
  },
89
89
  "engines": {
Binary file
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://biomejs.dev/schemas/2.5.10/schema.json",
2
+ "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json",
3
3
  "files": {
4
4
  "includes": [
5
5
  "**",