@jay-framework/jay-stack-cli 0.17.3 → 0.17.4

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.
@@ -89,6 +89,8 @@ Inside `<jay:...>`, bindings resolve to **that instance's** contract tags (not t
89
89
 
90
90
  Headfull components own their UI and can be made full-stack by adding a `contract` attribute.
91
91
 
92
+ Headfull FS components must be placed in `src/components/` — each component in its own subdirectory with `.ts`, `.jay-html`, and `.jay-contract` files. The production build only discovers server-side component modules from `src/components/` and `src/plugins/`. Placing them inside page directories will work in dev mode but fail in production.
93
+
92
94
  ### Import Declaration
93
95
 
94
96
  ```html
@@ -14,7 +14,7 @@ Entry point at `src/pages/`. Can import all component types.
14
14
 
15
15
  ### Headfull FS
16
16
 
17
- Reusable component with its own template + contract + three-phase rendering (slow/fast/interactive). Lives alongside the page in a components directory. Can nest other headfull FS and instance headless in its own `<head>`. Cannot use keyed headless.
17
+ Reusable component with its own template + contract + three-phase rendering (slow/fast/interactive). Must live in `src/components/` (not inside page directories) so the production build can discover and compile them. Can nest other headfull FS and instance headless in its own `<head>`. Cannot use keyed headless.
18
18
 
19
19
  ### Headless
20
20
 
@@ -14,6 +14,11 @@ my-project/
14
14
  │ ├── project.conf.yaml # Project metadata (name, etc.)
15
15
  │ └── <plugin-name>.yaml # Plugin-specific config files
16
16
  ├── src/
17
+ │ ├── components/ # Headfull full-stack components (shared across pages)
18
+ │ │ └── site-header/
19
+ │ │ ├── site-header.ts
20
+ │ │ ├── site-header.jay-html
21
+ │ │ └── site-header.jay-contract
17
22
  │ ├── pages/ # Pages (directory-based routing)
18
23
  │ │ ├── page.jay-html # Homepage → /
19
24
  │ │ ├── page.jay-contract # Homepage contract (optional)
@@ -64,7 +64,7 @@ Validate all `.jay-html` and `.jay-contract` files.
64
64
  jay-stack validate
65
65
 
66
66
  # Validate a specific path
67
- jay-stack validate src/pages/products/
67
+ jay-stack validate -p src/pages/products/
68
68
 
69
69
  # Verbose (per-file status)
70
70
  jay-stack validate -v
@@ -167,6 +167,10 @@ If not found, lists available actions:
167
167
  Available actions: searchProducts, getProductBySlug, getCategories
168
168
  ```
169
169
 
170
+ ## Production Commands
171
+
172
+ For `jay-stack build`, `jay-stack serve`, and `jay-stack rebuild`, see the [DevOps guides](../devops/INSTRUCTIONS.md).
173
+
170
174
  ## jay-stack dev
171
175
 
172
176
  Start the development server.
@@ -31,9 +31,13 @@ refs.submitButton.onClick(() => {
31
31
  /* ... */
32
32
  });
33
33
 
34
- // exec$ gives direct access to the element and current ViewState
35
- refs.submitButton.exec$((element, viewState) => {
36
- element.disabled = viewState.isSubmitting;
34
+ // exec$ gives direct access to the element and current ViewState.
35
+ // Only use exec$ inside event handlers — never at top-level component
36
+ // creation or in effects, because elements don't exist yet at that point.
37
+ refs.submitButton.onclick(() => {
38
+ refs.submitButton.exec$((element, viewState) => {
39
+ element.disabled = viewState.isSubmitting;
40
+ });
37
41
  });
38
42
  ```
39
43
 
@@ -14,6 +14,11 @@ my-project/
14
14
  │ ├── project.conf.yaml # Project metadata (name, etc.)
15
15
  │ └── <plugin-name>.yaml # Plugin-specific config files
16
16
  ├── src/
17
+ │ ├── components/ # Headfull full-stack components (shared across pages)
18
+ │ │ └── site-header/
19
+ │ │ ├── site-header.ts
20
+ │ │ ├── site-header.jay-html
21
+ │ │ └── site-header.jay-contract
17
22
  │ ├── pages/ # Pages (directory-based routing)
18
23
  │ │ ├── page.jay-html # Homepage → /
19
24
  │ │ ├── page.jay-contract # Homepage contract (optional)
@@ -0,0 +1,23 @@
1
+ # Jay Stack DevOps — Agent Kit
2
+
3
+ This folder contains guides for building, deploying, and operating jay-stack projects in production.
4
+
5
+ ## What Does the DevOps Role Do?
6
+
7
+ The devops role handles the production lifecycle: building artifacts, configuring deployment environments, serving in different modes (self-hosted, CDN, BaaS), and managing content invalidation. This is distinct from the developer role (creates page logic), designer role (creates UI), and plugin role (creates headless components).
8
+
9
+ ## Workflow
10
+
11
+ 1. **Build** — `jay-stack build` to compile all pages into production artifacts
12
+ 2. **Deploy** — upload `frontend/` to CDN, deploy `backend/` to server container
13
+ 3. **Serve** — start the production server with environment-appropriate flags
14
+ 4. **Invalidate** — rebuild specific pages when data changes
15
+
16
+ ## Guides
17
+
18
+ | File | Topic |
19
+ | ------------------------------------------ | -------------------------------------------------------- |
20
+ | [production-build.md](production-build.md) | Build pipeline, output structure, frontend/backend split |
21
+ | [serving-modes.md](serving-modes.md) | Self-hosted, CDN, BaaS (fetch handler), CLI flags |
22
+ | [fetch-handler.md](fetch-handler.md) | @jay-framework/jay-fetch-handler for BaaS integration |
23
+ | [invalidation.md](invalidation.md) | Rebuild, renderer server, cleanup |
@@ -0,0 +1,122 @@
1
+ # Fetch Handler Package
2
+
3
+ ## Overview
4
+
5
+ `@jay-framework/jay-fetch-handler` exports a standard `(Request) → Response` function for BaaS platforms (Wix, Cloudflare Workers) where an HTTP server is not needed — the platform provides the HTTP layer and calls the fetch function directly.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @jay-framework/jay-fetch-handler
11
+ ```
12
+
13
+ ## API
14
+
15
+ ```typescript
16
+ import { createJayFetchHandler } from '@jay-framework/jay-fetch-handler';
17
+
18
+ const handler = createJayFetchHandler(options);
19
+ // handler: (request: Request) => Promise<Response>
20
+ ```
21
+
22
+ ### Options
23
+
24
+ ```typescript
25
+ interface JayFetchHandlerOptions {
26
+ backendDir: string; // Path to build/v{n}/backend/
27
+ staticBaseUrl?: string; // Base URL for browser assets (default: '/')
28
+ frontendDir?: string; // When set, serves static files from this directory
29
+ }
30
+ ```
31
+
32
+ | Option | Required | Description |
33
+ | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
34
+ | `backendDir` | Yes | Path to the backend build directory containing manifest, server modules, and pre-rendered files |
35
+ | `staticBaseUrl` | No | URL prefix for import maps, CSS links, and client bundles. Set to your CDN URL for external hosting. Default: `/` |
36
+ | `frontendDir` | No | When provided, the handler serves static files from this directory. Omit for CDN deployments where static files are hosted elsewhere |
37
+
38
+ ## Usage — Wix BaaS
39
+
40
+ ```typescript
41
+ import { createJayFetchHandler } from '@jay-framework/jay-fetch-handler';
42
+
43
+ const handler = createJayFetchHandler({
44
+ backendDir: './build/v1/backend',
45
+ staticBaseUrl: 'https://static.parastorage.com/services/my-app/1.0.0/',
46
+ });
47
+
48
+ export default { fetch: handler };
49
+ ```
50
+
51
+ The BaaS runtime calls `handler(request)` for each incoming HTTP request.
52
+
53
+ ## Usage — Cloudflare Workers
54
+
55
+ ```typescript
56
+ import { createJayFetchHandler } from '@jay-framework/jay-fetch-handler';
57
+
58
+ const handler = createJayFetchHandler({
59
+ backendDir: './backend',
60
+ staticBaseUrl: 'https://cdn.example.com/assets/',
61
+ });
62
+
63
+ export default { fetch: handler };
64
+ ```
65
+
66
+ ## Usage — Standalone with HTTP Server
67
+
68
+ ```typescript
69
+ import { createJayFetchHandler } from '@jay-framework/jay-fetch-handler';
70
+ import http from 'node:http';
71
+ import { Readable } from 'node:stream';
72
+
73
+ const handler = createJayFetchHandler({
74
+ backendDir: './build/v1/backend',
75
+ staticBaseUrl: '/',
76
+ frontendDir: './build/v1/frontend',
77
+ });
78
+
79
+ http
80
+ .createServer(async (req, res) => {
81
+ const url = new URL(req.url, `http://${req.headers.host}`);
82
+ const headers = new Headers();
83
+ for (const [k, v] of Object.entries(req.headers)) {
84
+ if (v) headers.set(k, Array.isArray(v) ? v.join(', ') : v);
85
+ }
86
+ const init: RequestInit = { method: req.method, headers };
87
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
88
+ init.body = Readable.toWeb(req) as ReadableStream;
89
+ (init as any).duplex = 'half';
90
+ }
91
+ const request = new Request(url, init);
92
+ const response = await handler(request);
93
+
94
+ const resHeaders: Record<string, string> = {};
95
+ response.headers.forEach((v, k) => {
96
+ resHeaders[k] = v;
97
+ });
98
+ res.writeHead(response.status, resHeaders);
99
+ if (response.body) {
100
+ const reader = response.body.getReader();
101
+ while (true) {
102
+ const { done, value } = await reader.read();
103
+ if (done) break;
104
+ res.write(value);
105
+ }
106
+ }
107
+ res.end();
108
+ })
109
+ .listen(4000);
110
+ ```
111
+
112
+ This is what `jay-stack serve` does internally. Use the CLI for standard deployments; use the handler directly when you need custom server logic.
113
+
114
+ ## Behavior
115
+
116
+ The handler processes requests in this order:
117
+
118
+ 1. **Actions** — `/_jay/actions/*` routes to the action registry
119
+ 2. **Static files** — if `frontendDir` is set, checks `frontend/`, then `frontend/public/`
120
+ 3. **Page requests** — matches against the route manifest, runs fast-phase SSR, streams HTML
121
+
122
+ Initialization (loading manifest, running `init.ts`, registering actions) happens lazily on the first request.
@@ -0,0 +1,77 @@
1
+ # Invalidation & Rebuild
2
+
3
+ ## When to Rebuild
4
+
5
+ Page instances are pre-rendered at build time with slow-phase data. When that data changes (product updated, content edited), the affected instances need to be rebuilt without a full build.
6
+
7
+ ## jay-stack rebuild
8
+
9
+ Three targeting modes:
10
+
11
+ ```bash
12
+ # By contract — rebuild all routes using this contract
13
+ jay-stack rebuild --contract product-page
14
+
15
+ # By route — rebuild all instances of a route pattern
16
+ jay-stack rebuild --route /products/[slug]
17
+
18
+ # By URL — resolve to route+params, rebuild that one instance
19
+ jay-stack rebuild --url /products/blue-widget
20
+ ```
21
+
22
+ Narrow it down with `--params`:
23
+
24
+ ```bash
25
+ # Rebuild one specific instance
26
+ jay-stack rebuild --contract product-page --params '{"slug":"blue-widget"}'
27
+ jay-stack rebuild --route /products/[slug] --params '{"slug":"blue-widget"}'
28
+ ```
29
+
30
+ ### How it works
31
+
32
+ 1. Reads the route manifest to find affected routes/instances
33
+ 2. Re-runs slow render with fresh data
34
+ 3. Re-compiles server element and client bundle
35
+ 4. Updates the instance entry in the manifest
36
+ 5. Touches `build-metadata.json` to trigger main server manifest reload
37
+ 6. Old files are tracked in `cleanup-manifest.json` for later cleanup
38
+
39
+ ### Cleanup
40
+
41
+ After rebuilds, orphaned files (old client bundles, old server elements) accumulate. Clean them up:
42
+
43
+ ```bash
44
+ jay-stack cleanup
45
+ ```
46
+
47
+ ## Renderer Server
48
+
49
+ For automated invalidation, run the renderer server alongside the main server:
50
+
51
+ ```bash
52
+ # Main server — pages and actions
53
+ jay-stack serve --role main --port 4000
54
+
55
+ # Renderer server — listens for webhooks, triggers rebuilds
56
+ jay-stack serve --role renderer --port 4001
57
+ ```
58
+
59
+ The renderer server:
60
+
61
+ - Listens for data change webhooks from external systems (CMS, e-commerce)
62
+ - Determines which routes are affected (via contract name → route resolution)
63
+ - Runs `rebuild` for each affected instance
64
+ - The main server detects the updated `build-metadata.json` and reloads the manifest
65
+
66
+ ### Contract-based resolution
67
+
68
+ The manifest tracks which contracts each route uses. When a webhook says "product-page data changed", the renderer finds all routes that use the `product-page` contract and rebuilds their instances.
69
+
70
+ ## Build Output After Rebuild
71
+
72
+ Rebuilt instances write new files to both `frontend/` and `backend/`:
73
+
74
+ - `backend/pre-rendered/` — updated jay-html, cache, server element
75
+ - `frontend/pages/` — updated client bundle, CSS
76
+
77
+ Old files are not deleted immediately — they're tracked in `cleanup-manifest.json`. This avoids breaking in-flight requests that still reference old bundles.
@@ -0,0 +1,80 @@
1
+ # Production Build
2
+
3
+ ## Building
4
+
5
+ ```bash
6
+ jay-stack build
7
+ jay-stack build --version 2
8
+ jay-stack build --no-minify # debugging
9
+ jay-stack build -v # verbose output
10
+ ```
11
+
12
+ Version defaults to the `version` field in `package.json` (e.g., `"1.2.3"` → build version `10203`). Override with `--version`.
13
+
14
+ ## Build Output Structure
15
+
16
+ The build produces two directories under `build/v{n}/`:
17
+
18
+ ```
19
+ build/v{n}/
20
+ ├── frontend/ # Browser-facing assets (→ CDN or static serving)
21
+ │ ├── shared/ # Framework + plugin client chunks
22
+ │ │ ├── runtime-{hash}.js
23
+ │ │ ├── component-{hash}.js
24
+ │ │ └── shared-manifest.json
25
+ │ ├── pages/ # Per-page client bundles + CSS
26
+ │ │ ├── index/
27
+ │ │ │ ├── page-{hash}.js
28
+ │ │ │ └── page.css
29
+ │ │ └── products/[slug]/
30
+ │ │ ├── page_{hash}-{hash}.js
31
+ │ │ └── page_{hash}.css
32
+ │ └── public/ # Copied from project ./public
33
+ │ └── images/
34
+ │ └── logo.png
35
+
36
+ ├── backend/ # Server-only artifacts (→ container)
37
+ │ ├── route-manifest.json # All routes, instances, action registry
38
+ │ ├── build-metadata.json # Version, timestamp, instance count
39
+ │ ├── server/ # Compiled server code
40
+ │ │ ├── init.js
41
+ │ │ ├── pages/{route}/page.js
42
+ │ │ ├── components/{name}/{name}.js
43
+ │ │ ├── plugins/{name}/{name}.js
44
+ │ │ └── actions/{name}.actions.js
45
+ │ └── pre-rendered/ # SSR artifacts per instance
46
+ │ └── {route}/
47
+ │ ├── page.jay-html # Pre-rendered HTML template
48
+ │ ├── page.cache.json # Slow ViewState + carryForward
49
+ │ ├── page.server-element.js # Streaming SSR module
50
+ │ └── page-parts.json # Component wiring config
51
+ ```
52
+
53
+ ## Frontend vs Backend
54
+
55
+ | Directory | Contains | Deploy to |
56
+ | ----------- | -------------------------------------------------------------------------- | ------------------------- |
57
+ | `frontend/` | JS bundles, CSS, images — everything the browser loads | CDN or static file server |
58
+ | `backend/` | Server modules, pre-rendered HTML, manifests — everything the server reads | Container / server |
59
+
60
+ The build is **environment-agnostic**. The same output serves any deployment mode. `staticBaseUrl` (where browser assets are hosted) is a serve-time parameter, not baked into the build.
61
+
62
+ ## Manifest
63
+
64
+ `backend/route-manifest.json` contains:
65
+
66
+ - **routes** — pattern, segments, server module path, instances with params
67
+ - **instances** — `preRenderedPath` and `serverElementPath` (relative to `backend/`), `clientBundlePath` and `clientCssPath` (relative to `frontend/`)
68
+ - **actions** — server module paths, action names
69
+ - **sharedManifest** — maps package names to hashed filenames in `frontend/shared/`
70
+
71
+ ## Project Structure Requirements
72
+
73
+ For production builds to work correctly:
74
+
75
+ - **Headfull FS components** must be in `src/components/` (not inside page directories)
76
+ - **Headless plugins** must be in `src/plugins/`
77
+ - **Actions** must be in `src/actions/` with `*.actions.ts` naming
78
+ - **Init** must be at `src/init.ts`
79
+
80
+ The build discovers server-side modules from `src/pages/`, `src/components/`, `src/plugins/`, and `src/actions/`. Files outside these directories are not compiled for server use.
@@ -0,0 +1,91 @@
1
+ # Serving Modes
2
+
3
+ ## Overview
4
+
5
+ The production server supports three deployment modes, all using the same build output:
6
+
7
+ | Mode | Static files | Server | Use case |
8
+ | ---------------- | ------------------------------ | ----------------------------------------------------------- | ------------------------------------ |
9
+ | **Self-hosted** | Server serves from `frontend/` | `jay-stack serve` | Local testing, standalone deployment |
10
+ | **CDN** | Uploaded to external CDN | `jay-stack serve --static-base-url <url> --no-serve-static` | Production with CDN |
11
+ | **BaaS (fetch)** | Uploaded to CDN | `createJayFetchHandler()` | Wix, Cloudflare Workers |
12
+
13
+ ## Self-Hosted (Default)
14
+
15
+ The server serves both pages and static files. No external CDN needed.
16
+
17
+ ```bash
18
+ jay-stack build
19
+ jay-stack serve --port 4000
20
+ ```
21
+
22
+ Static files are served from `build/v{n}/frontend/` at these URL prefixes:
23
+
24
+ - `/shared/` — framework client chunks
25
+ - `/pages/` — per-page client bundles and CSS
26
+ - `/` — public folder assets (images, fonts, JSON)
27
+
28
+ ## CDN Mode
29
+
30
+ Static files are hosted on an external CDN. The server only handles page requests and actions.
31
+
32
+ ```bash
33
+ jay-stack build
34
+
35
+ # Upload frontend/ to CDN
36
+ # e.g., aws s3 sync build/v1/frontend/ s3://my-bucket/app/1.0.0/
37
+
38
+ # Start server with CDN URL
39
+ jay-stack serve --port 4000 \
40
+ --static-base-url https://cdn.example.com/app/1.0.0/ \
41
+ --no-serve-static
42
+ ```
43
+
44
+ The server generates import maps, CSS links, and client bundle URLs prefixed with `--static-base-url`. It does not serve static files itself.
45
+
46
+ ## CLI Flags
47
+
48
+ ### jay-stack serve
49
+
50
+ | Flag | Default | Description |
51
+ | ------------------------- | ------------------- | ----------------------------------------------------------- |
52
+ | `--port <n>` | `3000` | Server port |
53
+ | `--version <n>` | from package.json | Build version to serve |
54
+ | `--role <role>` | `main` | `main` (pages + actions) or `renderer` (webhooks + rebuild) |
55
+ | `--static-base-url <url>` | `/` | Base URL for all browser-facing assets |
56
+ | `--no-serve-static` | (serves by default) | Disable serving static files from `frontend/` |
57
+ | `--test-mode` | off | Enable `/_jay/health` and `/_jay/shutdown` endpoints |
58
+ | `-v, --verbose` | off | Verbose logging |
59
+
60
+ ### jay-stack build
61
+
62
+ | Flag | Default | Description |
63
+ | --------------- | ----------------- | -------------------------------- |
64
+ | `--version <n>` | from package.json | Build version number |
65
+ | `--no-minify` | minified | Disable minification (debugging) |
66
+ | `-v, --verbose` | off | Verbose logging |
67
+
68
+ ## Test Mode
69
+
70
+ When `--test-mode` is enabled, the server exposes:
71
+
72
+ | Endpoint | Method | Response |
73
+ | ---------------- | ------ | ---------------------------------------------------------- |
74
+ | `/_jay/health` | GET | `{"status":"ready","port":4000,"uptime":5.2}` |
75
+ | `/_jay/shutdown` | POST | `{"status":"shutting_down"}` — gracefully stops the server |
76
+
77
+ Use for smoke tests and CI pipelines. The dev server (`jay-stack dev --test-mode`) has the same endpoints.
78
+
79
+ ## Two-Server Architecture
80
+
81
+ For data-driven sites, run two servers:
82
+
83
+ ```bash
84
+ # Main server — handles page requests
85
+ jay-stack serve --role main --port 4000
86
+
87
+ # Renderer server — handles webhooks and rebuilds
88
+ jay-stack serve --role renderer --port 4001
89
+ ```
90
+
91
+ The renderer server listens for data change webhooks and rebuilds affected page instances. The main server picks up the updated artifacts automatically (it re-reads the manifest when `build-metadata.json` changes).
@@ -29,6 +29,7 @@ A plugin provides headless components (data + interactions, no UI) that project
29
29
  | [component-context.md](component-context.md) | Context hooks: provide, reactive, global |
30
30
  | [render-results.md](render-results.md) | phaseOutput, RenderPipeline, errors, redirects |
31
31
  | [actions-guide.md](actions-guide.md) | makeJayAction, makeJayQuery, .jay-action files |
32
+ | [webhooks-guide.md](webhooks-guide.md) | makeWebhook, data change invalidation, renderer server |
32
33
  | [services-guide.md](services-guide.md) | createJayService, makeJayInit |
33
34
  | [plugin-routes.md](plugin-routes.md) | Plugin-provided pages: routes, jay-html templates, page components |
34
35
  | [seo-guide.md](seo-guide.md) | SEO head tags: title, meta, OG, canonical via phaseOutput |
@@ -31,9 +31,13 @@ refs.submitButton.onClick(() => {
31
31
  /* ... */
32
32
  });
33
33
 
34
- // exec$ gives direct access to the element and current ViewState
35
- refs.submitButton.exec$((element, viewState) => {
36
- element.disabled = viewState.isSubmitting;
34
+ // exec$ gives direct access to the element and current ViewState.
35
+ // Only use exec$ inside event handlers — never at top-level component
36
+ // creation or in effects, because elements don't exist yet at that point.
37
+ refs.submitButton.onclick(() => {
38
+ refs.submitButton.exec$((element, viewState) => {
39
+ element.disabled = viewState.isSubmitting;
40
+ });
37
41
  });
38
42
  ```
39
43
 
@@ -35,6 +35,10 @@ actions:
35
35
  - name: addToCart
36
36
  action: add-to-cart.jay-action
37
37
 
38
+ webhooks:
39
+ - name: onProductChange
40
+ - name: onInventoryUpdate
41
+
38
42
  services:
39
43
  - name: my-store
40
44
  marker: MY_STORE_SERVICE_MARKER
@@ -114,6 +118,12 @@ tags:
114
118
  - `name` — Action name (used with `jay-stack action <plugin>/<action>`)
115
119
  - `action` — Path to `.jay-action` metadata file
116
120
 
121
+ ### Webhook Entry Fields
122
+
123
+ - `name` — Export name of the `makeWebhook()` constant (e.g., `onProductChange`)
124
+
125
+ Webhooks are exposed at `POST /_jay/webhooks/{webhookName}` on the renderer server. The `webhookName` comes from the `makeWebhook('plugin.event-name')` call, not from the export name. The export name in plugin.yaml tells the framework which export to load from the plugin module.
126
+
117
127
  ### Service Entry Fields
118
128
 
119
129
  - `name` — Service name (for identification in plugins-index)
@@ -183,6 +193,8 @@ my-plugin/
183
193
  │ ├── actions/
184
194
  │ │ ├── search-products.jay-action
185
195
  │ │ └── add-to-cart.jay-action
196
+ │ ├── webhooks/
197
+ │ │ └── on-product-change.ts
186
198
  │ ├── components/
187
199
  │ │ ├── product-page.ts
188
200
  │ │ └── product-search.ts
@@ -0,0 +1,124 @@
1
+ # Webhooks & Data Change Invalidation
2
+
3
+ Webhooks let external systems (CMS, database, etc.) notify the renderer server when data changes, triggering targeted rebuilds of affected pages.
4
+
5
+ ## makeWebhook
6
+
7
+ ```typescript
8
+ import { makeWebhook } from '@jay-framework/fullstack-component';
9
+
10
+ export const onProductChange = makeWebhook('wix-stores.product-change')
11
+ .withServices(PRODUCTS_SERVICE)
12
+ .withHandler(async (event, invalidate, productsService) => {
13
+ const slug = await productsService.resolveSlug(event.payload.itemId);
14
+ await invalidate('product-page', { slug });
15
+ });
16
+ ```
17
+
18
+ ## Builder API
19
+
20
+ ```typescript
21
+ makeWebhook('plugin-name.event-name')
22
+ .withServices(SERVICE1, SERVICE2) // Inject services
23
+ .withHandler(async (event, invalidate, svc1, svc2) => {
24
+ // event.type — webhook name
25
+ // event.payload — parsed JSON body from the HTTP request
26
+ // event.headers — HTTP headers
27
+ // invalidate(contractName, params?) — trigger rebuild
28
+ });
29
+ ```
30
+
31
+ ## The invalidate Function
32
+
33
+ The `invalidate` callback resolves contract names to routes and rebuilds affected instances:
34
+
35
+ ```typescript
36
+ // Rebuild a specific instance — finds routes using 'product-page' contract,
37
+ // rebuilds the instance matching { slug: 'blue-widget' }
38
+ await invalidate('product-page', { slug: 'blue-widget' });
39
+
40
+ // Rebuild ALL instances of routes using 'search-results' contract
41
+ await invalidate('search-results');
42
+ ```
43
+
44
+ **Contract-based, not route-based.** A plugin knows its own contract names but not the project's route structure. The framework resolves which routes use each contract via the `contracts` field in the route manifest.
45
+
46
+ ## Declaring in plugin.yaml
47
+
48
+ ```yaml
49
+ webhooks:
50
+ - name: onProductChange
51
+ - name: onInventoryUpdate
52
+ ```
53
+
54
+ The `name` field is the **export name** from the plugin module. The webhook URL is derived from the `makeWebhook()` name argument, not the export name.
55
+
56
+ ## URL Mapping
57
+
58
+ Webhooks are exposed on the renderer server at:
59
+
60
+ ```
61
+ POST /_jay/webhooks/{webhookName}
62
+ ```
63
+
64
+ Where `{webhookName}` is the first argument to `makeWebhook()`. Example:
65
+
66
+ ```
67
+ makeWebhook('wix-stores.product-change')
68
+ → POST /_jay/webhooks/wix-stores.product-change
69
+ ```
70
+
71
+ ## Optimistic Skip
72
+
73
+ After re-running the slow render, the framework compares the new pre-rendered jay-html with the existing one. If the template structure is unchanged (e.g., a price change updates ViewState but doesn't toggle a slow conditional), the server element compilation and Vite client build are skipped.
74
+
75
+ ## Project Webhooks
76
+
77
+ Projects can define webhooks in `src/webhooks/`:
78
+
79
+ ```typescript
80
+ // src/webhooks/on-content-update.ts
81
+ import { makeWebhook } from '@jay-framework/fullstack-component';
82
+
83
+ export const onContentUpdate = makeWebhook('cms.content-update').withHandler(
84
+ async (event, invalidate) => {
85
+ const { pageSlug } = event.payload as { pageSlug: string };
86
+ await invalidate('content-page', { slug: pageSlug });
87
+ },
88
+ );
89
+ ```
90
+
91
+ Project webhooks don't need plugin.yaml — they are discovered by scanning compiled files in `server/webhooks/`.
92
+
93
+ ## CLI Rebuild
94
+
95
+ For manual or CI-triggered rebuilds without a webhook:
96
+
97
+ ```bash
98
+ # By contract — rebuild all routes using this contract
99
+ jay-stack rebuild --contract product-page
100
+
101
+ # By contract + params — rebuild specific instance
102
+ jay-stack rebuild --contract product-page --params '{"slug":"blue-widget"}'
103
+
104
+ # By route — rebuild all instances of a route (useful for pages with page.ts, no contract)
105
+ jay-stack rebuild --route /products/[slug]
106
+
107
+ # By route + params — rebuild specific instance
108
+ jay-stack rebuild --route /products/[slug] --params '{"slug":"blue-widget"}'
109
+
110
+ # By URL — resolve URL to route+params, rebuild that instance
111
+ jay-stack rebuild --url /products/blue-widget
112
+ ```
113
+
114
+ ## Renderer Server
115
+
116
+ The renderer server (`jay-stack serve --role=renderer`) hosts webhook endpoints and a rebuild API:
117
+
118
+ ```
119
+ POST /_jay/webhooks/:name — Webhook handler (plugin-defined)
120
+ POST /_jay/rebuild — Programmatic rebuild { contract, route, url, params }
121
+ GET /_jay/status — Health check + build info
122
+ ```
123
+
124
+ The `POST /_jay/rebuild` endpoint accepts one of `contract`, `route`, or `url` in the JSON body.