@pikku/skills 0.12.22 → 0.12.26

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 (106) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +1 -1
  4. package/skills/pikku-a11y/SKILL.md +59 -0
  5. package/skills/pikku-addon/SKILL.md +2 -2
  6. package/skills/pikku-agent/SKILL.md +67 -316
  7. package/skills/pikku-agent/references/agents.md +299 -0
  8. package/skills/{pikku-ai-vercel/SKILL.md → pikku-agent/references/runner-vercel.md} +2 -24
  9. package/skills/{pikku-ai-voice/SKILL.md → pikku-agent/references/voice.md} +1 -22
  10. package/skills/pikku-architect/SKILL.md +265 -0
  11. package/skills/pikku-auth/SKILL.md +89 -0
  12. package/skills/{pikku-better-auth/SKILL.md → pikku-auth/references/better-auth.md} +126 -34
  13. package/skills/{pikku-jose/SKILL.md → pikku-auth/references/jose.md} +1 -23
  14. package/skills/{pikku-machine-auth/SKILL.md → pikku-auth/references/machine-auth.md} +0 -23
  15. package/skills/pikku-auth/references/permissions.md +261 -0
  16. package/skills/{pikku-security/SKILL.md → pikku-auth/references/sessions.md} +0 -20
  17. package/skills/pikku-build/SKILL.md +88 -0
  18. package/skills/{pikku-build-app/SKILL.md → pikku-build/references/app.md} +76 -24
  19. package/skills/{pikku-feature/SKILL.md → pikku-build/references/feature.md} +1 -8
  20. package/skills/{pikku-build-app → pikku-build}/references/multi-app.md +56 -1
  21. package/skills/{pikku-build-platform/SKILL.md → pikku-build/references/platform.md} +22 -37
  22. package/skills/{pikku-template-clone/SKILL.md → pikku-build/references/post-clone.md} +0 -6
  23. package/skills/{pikku-build-quick/SKILL.md → pikku-build/references/quick.md} +4 -20
  24. package/skills/{pikku-build-app → pikku-build}/references/ship.md +9 -3
  25. package/skills/pikku-concepts/SKILL.md +72 -7
  26. package/skills/pikku-concepts/references/concept-mapping.md +8 -8
  27. package/skills/pikku-deploy/SKILL.md +158 -0
  28. package/skills/{pikku-deploy-azure/SKILL.md → pikku-deploy/references/azure.md} +18 -50
  29. package/skills/pikku-deploy/references/cloudflare.md +104 -0
  30. package/skills/pikku-deploy/references/express.md +92 -0
  31. package/skills/{pikku-deploy-fastify/SKILL.md → pikku-deploy/references/fastify.md} +8 -32
  32. package/skills/{pikku-deploy-lambda/SKILL.md → pikku-deploy/references/lambda.md} +6 -27
  33. package/skills/{pikku-deploy-nextjs/SKILL.md → pikku-deploy/references/nextjs.md} +9 -33
  34. package/skills/pikku-deploy/references/uws.md +72 -0
  35. package/skills/pikku-deploy/references/ws.md +75 -0
  36. package/skills/pikku-emails/SKILL.md +3 -2
  37. package/skills/pikku-fabric/SKILL.md +47 -20
  38. package/skills/{pikku-fabric-debug/SKILL.md → pikku-fabric/references/debugging.md} +0 -6
  39. package/skills/pikku-i18n/SKILL.md +62 -207
  40. package/skills/{pikku-paraglide/SKILL.md → pikku-i18n/references/enum-labels.md} +0 -6
  41. package/skills/pikku-i18n/references/messages.md +218 -0
  42. package/skills/{pikku-rtl/SKILL.md → pikku-i18n/references/rtl.md} +3 -9
  43. package/skills/pikku-knowledge/SKILL.md +15 -0
  44. package/skills/pikku-kysely/SKILL.md +13 -13
  45. package/skills/pikku-list-query/SKILL.md +163 -0
  46. package/skills/pikku-meta/SKILL.md +58 -130
  47. package/skills/{pikku-deps/SKILL.md → pikku-meta/references/audit.md} +1 -17
  48. package/skills/pikku-meta/references/meta.md +114 -0
  49. package/skills/{pikku-versioning/SKILL.md → pikku-meta/references/versioning.md} +0 -26
  50. package/skills/pikku-middleware/SKILL.md +5 -5
  51. package/skills/pikku-n8n-import/SKILL.md +0 -1
  52. package/skills/pikku-permissions/SKILL.md +75 -229
  53. package/skills/pikku-react/SKILL.md +50 -298
  54. package/skills/pikku-react/references/client.md +313 -0
  55. package/skills/{pikku-react-query/SKILL.md → pikku-react/references/react-query.md} +2 -22
  56. package/skills/{pikku-workflows-client/SKILL.md → pikku-react/references/workflows.md} +1 -22
  57. package/skills/pikku-realtime/SKILL.md +110 -251
  58. package/skills/pikku-scenario/SKILL.md +60 -45
  59. package/skills/pikku-scenario/references/persona-run.md +148 -0
  60. package/skills/pikku-seo/SKILL.md +133 -0
  61. package/skills/pikku-service-backends/SKILL.md +154 -0
  62. package/skills/pikku-service-backends/references/aws.md +106 -0
  63. package/skills/pikku-service-backends/references/backblaze.md +57 -0
  64. package/skills/pikku-service-backends/references/mongodb.md +90 -0
  65. package/skills/pikku-service-backends/references/redis.md +75 -0
  66. package/skills/pikku-service-backends/references/schema.md +63 -0
  67. package/skills/pikku-services/SKILL.md +68 -291
  68. package/skills/{pikku-audit/SKILL.md → pikku-services/references/audit.md} +0 -22
  69. package/skills/{pikku-config/SKILL.md → pikku-services/references/config.md} +1 -25
  70. package/skills/{pikku-pino/SKILL.md → pikku-services/references/pino.md} +0 -20
  71. package/skills/pikku-services/references/services.md +272 -0
  72. package/skills/pikku-software-archaeology/README.md +5 -1
  73. package/skills/pikku-software-archaeology/SKILL.md +16 -2
  74. package/skills/{pikku-product-second-opinion/example/sample-report.md → pikku-software-archaeology/example/second-opinion-sample-report.md} +1 -1
  75. package/skills/pikku-software-archaeology/references/blueprint.schema.json +1 -1
  76. package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
  77. package/skills/{pikku-product-second-opinion/SKILL.md → pikku-software-archaeology/references/second-opinion.md} +4 -9
  78. package/skills/pikku-webhook/SKILL.md +224 -0
  79. package/skills/pikku-wiring/SKILL.md +180 -0
  80. package/skills/{pikku-websocket/SKILL.md → pikku-wiring/references/channel.md} +2 -35
  81. package/skills/{pikku-cli/SKILL.md → pikku-wiring/references/cli.md} +1 -33
  82. package/skills/{pikku-gateway-slack/SKILL.md → pikku-wiring/references/gateway-slack.md} +0 -23
  83. package/skills/{pikku-http/SKILL.md → pikku-wiring/references/http.md} +3 -39
  84. package/skills/{pikku-mcp/SKILL.md → pikku-wiring/references/mcp.md} +0 -33
  85. package/skills/{pikku-queue/SKILL.md → pikku-wiring/references/queue.md} +1 -33
  86. package/skills/pikku-wiring/references/realtime.md +265 -0
  87. package/skills/{pikku-rpc/SKILL.md → pikku-wiring/references/rpc.md} +0 -32
  88. package/skills/{pikku-schedule/SKILL.md → pikku-wiring/references/scheduler.md} +1 -35
  89. package/skills/{pikku-trigger/SKILL.md → pikku-wiring/references/trigger.md} +0 -43
  90. package/skills/pikku-workflow/SKILL.md +39 -2
  91. package/skills/pikku-aws/SKILL.md +0 -161
  92. package/skills/pikku-backblaze/SKILL.md +0 -104
  93. package/skills/pikku-deploy-cloudflare/SKILL.md +0 -123
  94. package/skills/pikku-deploy-express/SKILL.md +0 -122
  95. package/skills/pikku-deploy-uws/SKILL.md +0 -144
  96. package/skills/pikku-mongodb/SKILL.md +0 -113
  97. package/skills/pikku-product-second-opinion/README.md +0 -43
  98. package/skills/pikku-redis/SKILL.md +0 -99
  99. package/skills/pikku-schema-ajv/SKILL.md +0 -83
  100. package/skills/pikku-schema-cfworker/SKILL.md +0 -82
  101. package/skills/pikku-ws/SKILL.md +0 -87
  102. /package/skills/{pikku-build-app → pikku-build}/references/theming.md +0 -0
  103. /package/skills/{pikku-product-second-opinion/references/report-template.md → pikku-software-archaeology/references/second-opinion-report-template.md} +0 -0
  104. /package/skills/{pikku-cli/references/complete-example.md → pikku-wiring/references/cli-complete-example.md} +0 -0
  105. /package/skills/{pikku-http → pikku-wiring}/references/http-options.md +0 -0
  106. /package/skills/{pikku-realtime/references/other-routes.md → pikku-wiring/references/realtime-other-routes.md} +0 -0
@@ -1,25 +1,5 @@
1
- ---
2
- name: pikku-react-query
3
- description: 'Use the Pikku auto-generated React Query hooks (`usePikkuQuery`, `usePikkuMutation`, `usePikkuInfiniteQuery`) to call backend RPC functions from a React frontend with full type safety. TRIGGER when: writing React components that need to call a Pikku function, fetch data, mutate data, or paginate; user mentions React Query, useQuery, useMutation, or building a frontend that talks to a Pikku backend. DO NOT TRIGGER when: working on the backend (use pikku-rpc / pikku-feature) or wiring a non-React frontend.'
4
- installGroups: [client, fabric]
5
- ---
6
-
7
1
  # Pikku React Query Hooks
8
2
 
9
- ## Agent Operating Procedure
10
-
11
- Use this skill as an execution checklist, not reference material.
12
-
13
- 1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
14
- 2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
15
- 3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
16
- 4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
17
- 5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
18
-
19
- Pikku generates a typed React Query layer from your backend `expose: true`
20
- functions. You don't write `useQuery`/`useMutation` against `fetch`
21
- yourself — you call hooks named after RPCs and get full type inference for
22
- input + output.
23
3
 
24
4
  ## Discover what's available on the client
25
5
 
@@ -77,7 +57,7 @@ The two generated files come from `pikku.config.json`'s
77
57
  `clientFiles.fetchFile` and `clientFiles.rpcWiringsFile`. Hooks live in
78
58
  the file at `clientFiles.reactQueryFile` (typically `api.gen.ts`).
79
59
 
80
- `apiUrl()` is the shared server-URL helper — see **pikku-react**. Never
60
+ `apiUrl()` is the shared server-URL helper — see `references/client.md`. Never
81
61
  inline `?? 'http://localhost:3000'`: a deploy that supplies the URL as a
82
62
  runtime binding leaves `import.meta.env.VITE_API_URL` undefined in the
83
63
  bundle, so the fallback is the branch that actually runs.
@@ -206,7 +186,7 @@ instead.
206
186
  When the project defines any workflow, the same file also gains
207
187
  `useStartWorkflow(name)` (mutation → `{ runId }`), `useRunWorkflow(name)`
208
188
  (mutation → the workflow's output) and `useWorkflowStatus(name, runId?)` (query,
209
- disabled until `runId` is set). See the **pikku-workflows-client** skill.
189
+ disabled until `runId` is set). See `references/workflows.md`.
210
190
 
211
191
  ## Calling RPCs without React Query
212
192
 
@@ -1,26 +1,5 @@
1
- ---
2
- name: pikku-workflows-client
3
- description: 'Run Pikku workflows from a React frontend and track their progress. Covers `useRunWorkflow` (run-and-wait), `useStartWorkflow` (fire-and-poll), and `useWorkflowStatus` (live status). TRIGGER when: a React component needs to invoke or display the status of a Pikku workflow, the user mentions long-running tasks / background jobs / progress UI tied to a workflow, or asks how to start/track a workflow from the client. DO NOT TRIGGER when: the user is wiring the workflow itself (use pikku-workflow) or only making regular RPC calls (use pikku-react-query).'
4
- installGroups: [client]
5
- ---
6
-
7
1
  # Pikku Workflows — Client Hooks
8
2
 
9
- ## Agent Operating Procedure
10
-
11
- Use this skill as an execution checklist, not reference material.
12
-
13
- 1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
14
- 2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
15
- 3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
16
- 4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
17
- 5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
18
-
19
- When a project defines any workflow — DSL or `pikkuWorkflowGraph` — three
20
- React Query hooks are auto-generated alongside the standard RPC hooks. They handle
21
- the two common shapes: **run-and-wait** (short workflows where the
22
- client waits for the result) and **fire-and-poll** (long workflows where
23
- the client gets a `runId` and polls status).
24
3
 
25
4
  ## Discover what workflows exist
26
5
 
@@ -36,7 +15,7 @@ below.
36
15
 
37
16
  These hooks are generated into the same `api.gen.ts` as `usePikkuQuery` —
38
17
  no extra setup beyond `PikkuProvider` + `QueryClientProvider` (see the
39
- **pikku-react** and **pikku-react-query** skills).
18
+ `references/client.md` and `references/react-query.md`).
40
19
 
41
20
  ## `useRunWorkflow(name, options?)` — run and wait
42
21
 
@@ -1,288 +1,147 @@
1
1
  ---
2
2
  name: pikku-realtime
3
- description: 'Use Pikku''s realtime feature — typed pub/sub events over WebSocket (multi-topic) or SSE (single-topic, auto-cleanup). Covers declaring EventHubTopics, scaffolding the /events channel, the auto-generated `PikkuRealtime` client, and publishing events from a function. TRIGGER when: the user asks for realtime updates, pub/sub, push notifications, server-sent events, websocket events, eventhub, or "live" data on the frontend. DO NOT TRIGGER when: the user wants RPC-style request/response (use pikku-rpc / pikku-react-query) or a custom one-off WebSocket channel (use pikku-websocket).'
3
+ description: >-
4
+ Use when making ANY view live/realtime in a Pikku app — a board, shared list, dashboard, ticker, bidding room, live count — or when adding two-way chat/presence. Covers the DEFAULT event-hub SSE path and the two-way WebSocket channel.
5
+ TRIGGER when: the user wants live updates, realtime, "update without refresh", a live board/feed/ticker/room, presence, or chat; or when data that MORE THAN ONE signed-in user can change should reflect others
6
+ DO NOT TRIGGER when: a plain one-shot query/refetch is fine (data only one user changes, or a manual refresh is acceptable), or for background jobs (that is pikku-schedule/pikku-workflow).
7
+ installGroups: [core, client]
4
8
  ---
5
9
 
6
- # Pikku Realtime
10
+ # Pikku Realtime (SSE + WebSocket channels)
7
11
 
8
- ## Agent Operating Procedure
12
+ There is NOTHING to hand-roll and NOTHING to "find". The event-hub SSE transport
13
+ is already wired into every app, and the two patterns below ARE the realtime
14
+ templates. Start from them and rename — never grep the project for existing
15
+ `sse`/`eventHub` code to copy, never write a custom `EventSource`, and never
16
+ write a bespoke `sse: true` route for a plain live feed.
9
17
 
10
- Use this skill as an execution checklist, not reference material.
18
+ ## Pick the transport (almost always SSE)
11
19
 
12
- 1. Discover before editing. Run the relevant `pikku meta ... --json` command and inspect only the focused output you need.
13
- 2. Identify the source files that own the behavior. Do not start by reading generated output, `.pikku`, `node_modules`, vendored packages, or broad build artifacts.
14
- 3. Make the smallest source change that satisfies the task. Keep generated files generated, and avoid hand-editing SDKs, schema output, or typegen.
15
- 4. Validate with the narrowest relevant command first, then run `pikku-verify` or `pikku all` when functions, wirings, schemas, or generated clients may have changed.
16
- 5. If validation fails, fix the source cause and rerun validation. Do not paper over generated errors by editing generated files.
20
+ - **Server → client live updates → SSE via the event-hub.** This is the DEFAULT
21
+ for making any view live: a board, list, dashboard, ticker, feed, or a "room"
22
+ (a bidding room, sale room, live auction). The client only RECEIVES — the
23
+ change itself happens through a NORMAL HTTP RPC (`placeBid`, `updateLot`, …)
24
+ that publishes the new row.
25
+ - **Client → server push mid-session → a WebSocket channel.** ONLY when the
26
+ BROWSER must send up the socket without a page action: live chat messages,
27
+ typing indicators, cursors/presence.
17
28
 
18
- Most realtime UI is just typed pub/sub: a server pushes `todo-created`, the client
19
- renders it. Pikku ships exactly that, two ways — both use the same `EventHubService`
20
- and the same publish call, so choose by transport, not by code shape:
29
+ A screen being called a "room", or being multi-user, or being live is NOT a
30
+ reason to use a channel. If the browser isn't pushing frames up, it's SSE.
21
31
 
22
- - **WebSocket** at `/events` — one connection, many topic subscriptions.
23
- - **SSE** at `GET /events/:topic` — one connection per topic, auto-cleanup on
24
- disconnect. Good when WebSocket is blocked or for trivially streaming one topic.
32
+ ## Level 1 — live updates (event-hub SSE, the default)
25
33
 
26
- ## 1. Declare your topics
34
+ Two halves; both are required or nothing arrives.
27
35
 
28
- In your project's types file (e.g. `types/eventhub-topics.d.ts`):
36
+ **Backend — publish after every write.** In each create/update/status function,
37
+ AFTER the DB write, publish the changed row on a topic:
29
38
 
30
39
  ```ts
31
- import type { Todo } from '../src/schemas.js'
32
-
33
- export type EventHubTopics = {
34
- 'todo-created': { todo: Todo }
35
- 'todo-updated': { todo: Todo }
36
- 'todo-deleted': { todoId: string }
37
- }
38
- ```
39
-
40
- Reference it in `application-types.d.ts` and instantiate it in `services.ts`:
41
-
42
- ```ts
43
- // application-types.d.ts
44
- import type { EventHubService } from '@pikku/core/channel'
45
- import type { EventHubTopics } from './eventhub-topics.js'
46
-
47
- export interface SingletonServices extends CoreSingletonServices<Config> {
48
- // `CoreSingletonServices` declares eventHub optional; re-declare it required
49
- // so functions can use it without a `if (eventHub)` guard on every publish.
50
- eventHub: EventHubService<EventHubTopics>
51
- }
52
-
53
- // services.ts
54
- import { LocalEventHubService } from '@pikku/core/channel'
55
- const eventHub = new LocalEventHubService<EventHubTopics>()
56
- ```
57
-
58
- For multi-instance deployments use `CloudflareEventHubService` /
59
- `LambdaEventHubService` / `UWSEventHubService` instead — same interface.
60
-
61
- If a deployment genuinely has no eventHub, that belongs in `services.ts` (don't
62
- create the service there), not as an optional type every function has to guard —
63
- see `pikku-services`.
64
-
65
- ## 2. Enable the server side
66
-
67
- ```bash
68
- yarn pikku enable events
40
+ const lot = await kysely
41
+ .updateTable('lot')
42
+ .set({ status: 'sold' })
43
+ .where('id', '=', input.lotId)
44
+ .returning(['id', 'status', 'currentBid', 'updatedAt'])
45
+ .executeTakeFirstOrThrow()
46
+ await eventHub.publish('lot-updated', null, { topic: 'lot-updated', data: lot })
47
+ return lot
69
48
  ```
70
49
 
71
- This sets `scaffold.events` in `pikku.config.json`. The next `pikku all` generates
72
- `events.gen.ts` in your scaffold dir, wiring (using whatever `eventHub` is in your
73
- singletons — you write neither by hand):
74
-
75
- - A WebSocket channel at `/events` handling `{action: 'subscribe' | 'unsubscribe', topic}` messages.
76
- - An SSE handler at `GET /events/:topic`.
77
-
78
- ## 3. Generate the typed client
50
+ **A topic is PUBLIC — publish a projection, never `returningAll()`.** The generated
51
+ `/events/:topic` route is wired `auth: false` with a sessionless handler, so anyone who can
52
+ reach the origin can subscribe to any topic name and read every frame on it. `returningAll()`
53
+ then ships the whole row — `reservePrice`, `sellerId`, internal notes, whatever the table
54
+ grows next — to unauthenticated subscribers, and it does it silently because the RPC's own
55
+ `output` schema never sees the event payload. List the columns the topic is FOR, the way the
56
+ example does. If a change genuinely has per-viewer content, it does not belong on a topic:
57
+ publish an id-only "something changed" frame and let each client refetch through an
58
+ authenticated RPC that applies its own permissions.
59
+
60
+ The **2nd arg is the channel to EXCLUDE** from the broadcast: pass `null` from a
61
+ normal HTTP/RPC write (there is no one to skip); pass `channel.channelId` ONLY
62
+ when you publish from INSIDE a channel handler, or the sender gets an echo of its
63
+ own update. `eventHub` is already injected — do not wire it.
64
+
65
+ **Frontend — subscribe over SSE.** The generated
66
+ `PikkuRealtime.subscribeToTopic(topic, handler)` opens an SSE stream to the
67
+ built-in `/events/:topic` route. Seed state from a normal query, then patch it as
68
+ events arrive; the event is the `{ topic, data }` envelope, so read `.data`.
79
69
 
80
- Add to `pikku.config.json`:
81
-
82
- ```jsonc
83
- {
84
- "clientFiles": {
85
- "realtimeFile": "packages/sdk/src/pikku/realtime.gen.ts",
86
- // Optional: full type inference for subscribe/unsubscribe
87
- "realtimeEventHubTopicsImport": "../../../functions/types/eventhub-topics.js#EventHubTopics",
88
- },
89
- }
90
- ```
91
-
92
- Run `pikku all` (or `pikku realtime` to regenerate just this file). Everything is
93
- on one class — both transports are methods, so switching from WebSocket to SSE is
94
- a one-word change, not a different import:
95
-
96
- ```ts
97
- export class PikkuRealtime {
98
- constructor(options?: {
99
- reconnect?: boolean
100
- reconnectDelayMs?: number
101
- reconnectMaxDelayMs?: number
102
- })
103
- setPikkuFetch(fetch: PikkuFetch): void // server URL + auth come from here, not the constructor
104
-
105
- // WebSocket at /events — many topics on one connection
106
- subscribe<K extends keyof EventHubTopics>(
107
- topic: K,
108
- handler: (data: EventHubTopics[K]) => void
109
- ): () => void
110
- unsubscribe<K extends keyof EventHubTopics>(
111
- topic: K,
112
- handler?: (data: EventHubTopics[K]) => void
113
- ): void
114
-
115
- // SSE at GET /events/:topic — one EventSource per topic
116
- subscribeToTopic<K extends keyof EventHubTopics>(
117
- topic: K,
118
- handler: (data: EventHubTopics[K]) => void
119
- ): { close: () => void }
70
+ ```tsx
71
+ import { useEffect } from 'react'
72
+ import { useQueryClient } from '@tanstack/react-query'
73
+ import { realtime } from '../lib/pikku'
120
74
 
121
- // generic escape hatches — see references/other-routes.md
122
- subscribeToSSE<T>(
123
- path: string,
124
- handler: (data: T) => void
125
- ): { close: () => void }
126
- connectToChannel(
127
- channelRoute: string,
128
- protocols?: string | string[]
129
- ): WebSocket
75
+ export function useLiveLots() {
76
+ const queryClient = useQueryClient()
130
77
 
131
- close(): void
78
+ useEffect(() => {
79
+ const subscription = realtime.subscribeToTopic('lot-updated', () => {
80
+ queryClient.invalidateQueries({ queryKey: ['listLots'] })
81
+ })
82
+ return () => subscription.close()
83
+ }, [queryClient])
132
84
  }
133
85
  ```
134
86
 
135
- Without `realtimeEventHubTopicsImport`, the client falls back to
136
- `Record<string, unknown>` — usable but untyped. Set the import for full typed
137
- subscribe/unsubscribe.
87
+ **Invalidate; do not hand-patch the cache.** The generated hooks key a query as
88
+ `[name, input]` — `['listLots', { status: 'open', cursor: undefined }]`, one entry per set of
89
+ arguments — so `setQueryData(['listLots'], …)` writes to a key nothing reads and the screen
90
+ never changes. `invalidateQueries({ queryKey: ['listLots'] })` prefix-matches, so it refreshes
91
+ every variant of that list whatever input each one was fetched with.
138
92
 
139
- ## 4. Publish events from a function
93
+ Patching also has to know the payload's shape, and a list RPC returns
94
+ `ListOutput<Lot>` — `{ rows, nextCursor, totalCount? }`, not `Lot[]` — so a `rows.map(...)`
95
+ updater is reading `.map` off an object. Refetching sidesteps both, and it re-applies the server's own filtering,
96
+ which a locally patched row does not: a lot that just moved to `sold` may no longer belong in
97
+ an "open lots" list at all.
140
98
 
141
- The `/events` channel listens for client subscriptions; the eventHub fans out
142
- publishes:
143
-
144
- ```ts
145
- publish(topic, channelId: string | null, data, isBinary?)
146
- ```
99
+ `subscribeToTopic` returns `{ close }` — ALWAYS close on unmount or you leak the
100
+ stream. Never hand-roll an `EventSource`.
147
101
 
148
- The middle argument is the channel to **skip**, not the one to send to — pass
149
- `null` to reach every subscriber, or the current `channel.channelId` when the
150
- originating connection has already applied the change locally and would otherwise
151
- render it twice.
102
+ ## Level 2 — two-way channel
152
103
 
153
- Envelope the payload as `{ topic, data }`: the generated client dispatches on the
154
- `topic` field, so a bare payload arrives but no handler fires.
104
+ Only when the client pushes up the socket. The backend channel lives in its own
105
+ `*.channel.ts` with `onConnect`/`onMessage` handlers:
155
106
 
156
107
  ```ts
157
- import { pikkuFunc } from '#pikku/function'
108
+ import { pikkuChannelFunc, wireChannel } from '#pikku/channel'
158
109
 
159
- export const createTodo = pikkuFunc({
160
- input: CreateTodoInput,
161
- output: CreateTodoOutput,
162
- func: async ({ kysely, eventHub }, data) => {
163
- const todo = await kysely
164
- .insertInto('todos')
165
- .values(data)
166
- .returningAll()
167
- .executeTakeFirstOrThrow()
168
-
169
- await eventHub.publish('todo-created', null, {
170
- topic: 'todo-created',
171
- data: { todo },
172
- })
173
- return { id: todo.id }
110
+ export const onMessage = pikkuChannelFunc<{ text: string }>({
111
+ func: async ({ eventHub }, input, { channel, session }) => {
112
+ const message = { id: crypto.randomUUID(), text: input.text, userId: session!.userId }
113
+ await eventHub.publish('room', channel.channelId, { topic: 'room', data: message })
114
+ return message
174
115
  },
175
116
  })
176
- ```
177
-
178
- A thin helper removes the duplication:
179
-
180
- ```ts
181
- async function publishEvent<K extends keyof EventHubTopics>(
182
- hub: EventHubService<EventHubTopics>,
183
- topic: K,
184
- data: EventHubTopics[K]
185
- ) {
186
- return hub.publish(topic, null, { topic, data })
187
- }
188
- // usage: await publishEvent(eventHub, 'todo-created', { todo })
189
- ```
190
-
191
- ## 5. Wire it up — share fetch with PikkuRPC
192
-
193
- `PikkuRealtime` mirrors `PikkuRPC`: it wraps the same `PikkuFetch`, so server URL +
194
- auth are configured **once** and shared across HTTP, RPC, and realtime transports.
195
-
196
- ```tsx
197
- import { createPikku, PikkuProvider } from '@pikku/react'
198
- import { PikkuFetch } from './pikku/pikku-fetch.gen'
199
- import { PikkuRPC } from './pikku/pikku-rpc.gen'
200
- import { PikkuRealtime } from './pikku/realtime.gen'
201
-
202
- const pikku = createPikku(
203
- PikkuFetch,
204
- PikkuRPC,
205
- PikkuRealtime, // pass the realtime class as the third arg
206
- { serverUrl: apiUrl() } // shared env helper — see pikku-react
207
- )
208
- // pikku.fetch / pikku.rpc / pikku.realtime — all share the same fetch.
209
-
210
- createRoot(document.getElementById('root')!).render(
211
- <PikkuProvider pikku={pikku}>
212
- <App />
213
- </PikkuProvider>
214
- )
215
- ```
216
117
 
217
- Or wire manually:
218
-
219
- ```ts
220
- const realtime = new PikkuRealtime()
221
- realtime.setPikkuFetch(pikku.fetch) // inherits serverUrl + auth
118
+ wireChannel({ name: 'room', route: '/room', auth: true, onMessage })
222
119
  ```
223
120
 
224
- ## 6. Subscribe from React
225
-
226
- Subscribe inside `useEffect` (never the render path, or you create a subscription
227
- per render). `subscribe` returns an unsubscribe function; SSE's `subscribeToTopic`
228
- returns a handle with `close()`:
121
+ The frontend opens it with `PikkuRealtime.connectToChannel(path)`, which returns
122
+ a socket you both `.send(...)` on and read via `onmessage`:
229
123
 
230
124
  ```tsx
231
- import { useEffect, useState } from 'react'
232
-
233
- function TodoList() {
234
- const { realtime } = usePikku() // a hook over your context
235
- const [todos, setTodos] = useState<Todo[]>([])
236
-
237
- useEffect(() => {
238
- // WebSocket multi-topic:
239
- const off = realtime.subscribe('todo-created', ({ todo }) =>
240
- setTodos((prev) => [...prev, todo])
241
- )
242
- return off
243
-
244
- // Single-topic SSE (auto-cleanup on close) instead:
245
- // const sub = realtime.subscribeToTopic('todo-created', ({ todo }) =>
246
- // setTodos((prev) => [...prev, todo]))
247
- // return () => sub.close()
248
- }, [realtime])
249
-
250
- return (
251
- <ul>
252
- {todos.map((t) => (
253
- <li key={t.id}>{t.title}</li>
254
- ))}
255
- </ul>
256
- )
257
- }
125
+ useEffect(() => {
126
+ const socket = realtime.connectToChannel('/room')
127
+ socket.onmessage = (event) => appendMessage(JSON.parse(event.data))
128
+ return () => socket.close()
129
+ }, [])
258
130
  ```
259
131
 
260
- ## Other SSE / WebSocket routes
261
-
262
- The same client also subscribes to generic `sse: true` routes and raw `wireChannel`
263
- sockets (`subscribeToSSE`, `connectToChannel`). See
264
- [references/other-routes.md](references/other-routes.md).
265
-
266
- ## When to pick which transport
267
-
268
- | Need | Use |
269
- | ------------------------------------------ | --------------------------- |
270
- | Many topics in one connection | `realtime.subscribe` |
271
- | Single live stream, simple cleanup | `realtime.subscribeToTopic` |
272
- | Bidirectional (client also sends messages) | `realtime.subscribe` |
273
- | WebSockets blocked by infra | `realtime.subscribeToTopic` |
274
-
275
- Both auto-clean on the server (the eventHub's `onChannelClosed` hook unsubscribes
276
- all topics for the dead channel id). Don't write manual cleanup unless you're
277
- unsubscribing partway through a session.
278
-
279
- ## What NOT to do
280
-
281
- - Don't call `eventHub.publish(topic, ..., rawData)` without the `{topic, data}`
282
- envelope — clients use `topic` to dispatch handlers.
283
- - Don't create your own `/events` channel by hand — `pikku enable events` already
284
- does it correctly with disconnect cleanup.
285
- - Don't subscribe inside the render path — use `useEffect`.
286
- - Don't subscribe to topics that don't exist in `EventHubTopics`. The generated
287
- client's types prevent it; if you reach for `as any` to subscribe to a string,
288
- declare the topic first.
132
+ Publish server→client fan-out from a channel handler with
133
+ `eventHub.publish(topic, channel.channelId, envelope)` — the 2nd arg excludes the
134
+ sender, so the browser that sent the message does not receive its own echo.
135
+
136
+ ## Do NOT
137
+
138
+ - Do **not** grep the project for existing SSE/eventHub infra to reverse-engineer
139
+ or copy — the patterns above ARE the template (same rule as never reading
140
+ `.gen.ts` to learn an API).
141
+ - Do **not** write a custom `sse: true` HTTP route or a bespoke `EventSource` for
142
+ an ordinary live feed — the event-hub covers it. (A dedicated `sse: true` route
143
+ is only for a long-job PROGRESS stream, and is not needed for an initial build.)
144
+ - Do **not** use a WebSocket channel for a live board/ticker/room — that is SSE.
145
+ A channel is for client→server push (chat/presence) ONLY.
146
+ - Do **not** forget the backend `eventHub.publish(...)` — a subscribed frontend
147
+ with no publisher is a silent, empty stream.