diff --git a/.agents/skills/webjs-doc-sync/SKILL.md b/.agents/skills/webjs-doc-sync/SKILL.md index 19f19209b..ba3b28b63 100644 --- a/.agents/skills/webjs-doc-sync/SKILL.md +++ b/.agents/skills/webjs-doc-sync/SKILL.md @@ -42,7 +42,7 @@ applies, then update or consciously skip each. 2. **`README.md`** (repo root). Update when a headline capability changes (the feature list, the quickstart, the runtime/template matrix). 3. **The docs site: `docs/app/docs//page.tsx`.** This is the - user-facing documentation at docs.webjs.com. Find the topic page(s) that cover + user-facing documentation at docs.webjs.dev. Find the topic page(s) that cover the area (`server-actions`, `routing`, `components`, `caching`, `configuration`, `client-router`, `data-fetching`, ...) and update them. `llms.txt` / `llms-full.txt` are generated LIVE from the doc pages (no build step), so they diff --git a/.claude/hooks/require-scaffold-with-src.sh b/.claude/hooks/require-scaffold-with-src.sh new file mode 100755 index 000000000..f76909af5 --- /dev/null +++ b/.claude/hooks/require-scaffold-with-src.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# +# PreToolUse hook: BLOCK a `git commit` that changes framework FEATURE +# source but stages NO scaffold surface. The scaffold is webjs's PRIMARY +# teaching surface for AI agents: an agent learns the framework by reading +# the generated gallery/showcase and its comments, then builds the real app +# by adapting them. So when a webjs FEATURE is added or changed, the scaffold +# that teaches it must move too, exactly as the docs must (this is the +# scaffold twin of require-docs-with-src.sh). +# +# It fires on every commit, decides from the STAGED diff (not model +# judgement), and BLOCKS (exit 2) when framework-feature source is staged +# with no scaffold surface in the same commit. +# +# What a hook CANNOT do: know WHETHER a given change actually needs a +# scaffold demo (a bug fix, an internal perf change, or a tweak to an +# already-demoed feature may not). So it enforces the floor (a +# feature-source commit must stage SOME scaffold surface OR consciously opt +# out) and points at the webjs-scaffold-sync skill for the per-surface walk +# and the mandatory generate-boot-check. +# +# Scope: only fires on `git commit` Bash calls. Inspects the STAGED diff, +# so `git add` choices drive it. +# +# Trigger (feature source that MAY need a scaffold update): the runtime + +# CLI packages' src, packages/(core|server|cli)/src/**. Editor plugins, the +# MCP, and intellisense do not shape the scaffold, so they are excluded. +# +# Satisfying surface: the scaffold itself, packages/cli/templates/** (the +# template files + per-agent rule files) OR packages/cli/lib/** (the +# create.js / saas-template.js / api-gallery.js generators). +# +# Allowed (exit 0): commits touching no feature src; commits that stage a +# scaffold surface alongside the source; and a change that genuinely needs +# no scaffold update (a bug fix, an internal refactor, a tweak to an +# already-demoed feature) via WEBJS_NO_SCAFFOLD_GATE=1. +# +# Rule: AGENTS.md "Code workflow" (Scaffold) + the webjs-scaffold-sync skill. + +set -euo pipefail + +if [ "${WEBJS_NO_SCAFFOLD_GATE:-}" = "1" ]; then + exit 0 +fi + +payload=$(cat) +cmd=$(printf '%s' "$payload" | jq -r '.tool_input.command // empty' 2>/dev/null || true) +if [ -z "$cmd" ]; then exit 0; fi + +# Match `git commit` as a whole word (a real commit, not git commit-tree). +if ! printf '%s' "$cmd" | grep -Eq '(^|[^[:alnum:]-])git commit([^[:alnum:]-]|$)'; then + exit 0 +fi + +if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then exit 0; fi + +staged=$(git diff --cached --name-only 2>/dev/null || true) +if [ -z "$staged" ]; then exit 0; fi + +# Framework FEATURE source: the runtime + CLI packages' src. A change here is +# the kind that MAY need the scaffold that teaches it to move too. +src_touched=$(printf '%s\n' "$staged" | grep -E '^packages/(core|server|cli)/src/' || true) +if [ -z "$src_touched" ]; then exit 0; fi + +# Any scaffold surface staged in the same commit? The scaffold is the +# templates (files + per-agent rule files) and the generators (cli/lib). +scaffold_staged=$(printf '%s\n' "$staged" | grep -E '^packages/cli/(templates|lib)/' || true) +if [ -n "$scaffold_staged" ]; then exit 0; fi + +cat >&2 <<'EOF' +BLOCKED: this commit changes framework-feature source but stages no scaffold. + +Staged source under packages/(core|server|cli)/src/** with no scaffold +surface in the same commit. The scaffold is webjs's PRIMARY teaching surface +for AI agents (they learn the framework by reading the generated +gallery/showcase, then build the real app by adapting it), so a task is NOT +done until the scaffold that teaches the changed feature is in sync too. + +If this added or changed a webjs FEATURE an agent should learn from the +scaffold (a new export/API, an html hole, a lifecycle hook, a server-action +capability, a config key, a CLI behaviour), invoke the webjs-scaffold-sync +skill, then `git add` the scaffold surface(s) and commit again: + packages/cli/templates/gallery/ a UI feature-gallery demo (full-stack + saas) + packages/cli/lib/api-gallery.js an api backend-showcase endpoint + packages/cli/lib/{create,saas-template}.js the generators (home, theme, schema, wiring) + packages/cli/templates/ (AGENTS/CONVENTIONS/.cursorrules/...) scaffold rules in lockstep + test/scaffolds/** the scaffold assertions for the above +The skill walks every surface AND runs the mandatory generate + boot + +`webjs check` verification (the generators emit strings, so escaping bugs +only show in a freshly generated app). + +Change that genuinely needs no scaffold update (a bug fix, an internal +refactor, a tweak to an already-demoed feature)? Re-run with +WEBJS_NO_SCAFFOLD_GATE=1. + +Hook: .claude/hooks/require-scaffold-with-src.sh +EOF +exit 2 diff --git a/.claude/hooks/route-skills.sh b/.claude/hooks/route-skills.sh index cfa184952..dc25c7bec 100755 --- a/.claude/hooks/route-skills.sh +++ b/.claude/hooks/route-skills.sh @@ -128,6 +128,25 @@ if has '(doc|documentation) (gap|drift|sync|coverage|debt)' \ add_match "webjs-doc-sync: the request is about documentation sync, drift, or a doc gap. Invoke the webjs-doc-sync skill BEFORE editing any doc. It holds the authoritative map of EVERY surface (AGENTS.md + agent-docs/, README, the docs site under docs/app/docs/, the marketing website/, and the scaffold templates' per-agent rule files) and the change-type to surface mapping, so no surface is silently skipped. File each confirmed gap via webjs-file-issue." fi +# --- webjs-scaffold-sync: keep every scaffold surface in sync ----------- +# Triggers: change what `webjs create` generates (a gallery/showcase demo, a +# template, the generated layout/home/theme/schema), sync the scaffold, +# "update all three templates", check the scaffold is consistent, teach +# agents via the scaffold. The scaffold is webjs's PRIMARY teaching surface, +# so a change must propagate across the generators (packages/cli/lib/*), the +# per-agent rule files, the scaffold tests, the framework template-matrix +# docs, and the preview apps, with a mandatory "generate + boot + check". +if has '(sync|update|check|fix|add to).{0,24}(the )?(scaffold|template)' \ + || has '(all (three|3) )?(scaffold )?templates?\b' \ + || has 'gallery.{0,40}(template|saas|api|full-?stack|scaffold)' \ + || has '(ship|include|add|put).{0,30}(the )?(gallery|showcase)' \ + || has '(add|new|update).{0,24}(gallery|showcase)' \ + || has '(scaffold|gallery) (consistent|in sync|drift|gap)' \ + || has 'what `?webjs create`? generates' \ + || has '(feature|backend) gallery'; then + add_match "webjs-scaffold-sync: the request changes what \`webjs create\` generates (a gallery/showcase demo, a template, a generated file, a scaffold convention). Invoke the webjs-scaffold-sync skill BEFORE editing. It holds the authoritative map of every scaffold surface (the packages/cli/lib/* generators, the templates/, the per-agent rule files in lockstep, the scaffold tests, the framework template-matrix docs, the preview apps) plus the mandatory generate-boot-check verification, so no surface is silently skipped. It is the scaffold-side sibling of webjs-doc-sync." +fi + # --- code-review: review the diff before a PR is ready ------------------ # Triggers: review the PR/diff/branch/changes, code review, look it over # for bugs. Reviewing every change before it is marked ready is a standing diff --git a/.claude/settings.json b/.claude/settings.json index 18137e2eb..3df01041d 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -40,6 +40,10 @@ "type": "command", "command": ".claude/hooks/require-docs-with-src.sh" }, + { + "type": "command", + "command": ".claude/hooks/require-scaffold-with-src.sh" + }, { "type": "command", "command": ".claude/hooks/require-bun-parity-with-runtime-src.sh" diff --git a/.claude/skills/webjs-doc-sync/SKILL.md b/.claude/skills/webjs-doc-sync/SKILL.md index 19f19209b..dccc8a0c5 100644 --- a/.claude/skills/webjs-doc-sync/SKILL.md +++ b/.claude/skills/webjs-doc-sync/SKILL.md @@ -42,7 +42,7 @@ applies, then update or consciously skip each. 2. **`README.md`** (repo root). Update when a headline capability changes (the feature list, the quickstart, the runtime/template matrix). 3. **The docs site: `docs/app/docs//page.tsx`.** This is the - user-facing documentation at docs.webjs.com. Find the topic page(s) that cover + user-facing documentation at docs.webjs.dev. Find the topic page(s) that cover the area (`server-actions`, `routing`, `components`, `caching`, `configuration`, `client-router`, `data-fetching`, ...) and update them. `llms.txt` / `llms-full.txt` are generated LIVE from the doc pages (no build step), so they @@ -59,7 +59,13 @@ applies, then update or consciously skip each. `.gemini` / `.opencode` / `.claude` rule files. These per-agent files all carry the SAME rules in each agent's format; a workflow/convention change must land in ALL of them in lockstep (the #134 / #136 divergence lesson). The CLI help text - in `packages/cli/` is part of this surface for a new command or flag. + in `packages/cli/` is part of this surface for a new command or flag. When the + change is to what `webjs create` GENERATES (a gallery/showcase demo, a + template, the generated layout/home/theme/schema, a scaffold convention), + this surface has more parts (the `packages/cli/lib/*` generators, the scaffold + tests, the framework template-matrix docs, the preview apps) and a mandatory + "generate + boot + check" step: use the dedicated **`webjs-scaffold-sync`** + skill for those, and treat this doc-sync entry as the docs-only slice. 6. **Example / dogfood apps** (`examples/blog/CONVENTIONS.md` and friends). Update when a convention the example demonstrates changes. diff --git a/.claude/skills/webjs-scaffold-sync/SKILL.md b/.claude/skills/webjs-scaffold-sync/SKILL.md new file mode 100644 index 000000000..6686f34c6 --- /dev/null +++ b/.claude/skills/webjs-scaffold-sync/SKILL.md @@ -0,0 +1,157 @@ +--- +name: webjs-scaffold-sync +description: Use this skill whenever a change affects what `webjs create` GENERATES (a new or changed gallery/showcase demo, a new template, a changed generated file like the layout/home/theme/schema, a new convention that belongs in generated apps, a new scaffold-shipped config/hook) OR when the user asks to sync the scaffold, "update all three templates", check the scaffold is consistent, or teach agents how to use webjs through the scaffold. The scaffold is webjs's PRIMARY teaching surface for AI agents, so a change to it must propagate in lockstep across the generators, the per-agent rule files, the scaffold tests, the framework docs that describe the scaffold, and the preview/example apps. This skill is the authoritative map of every scaffold surface plus the change-type to surface mapping, and the mandatory "generate + boot + check" verification, so no surface is silently skipped. Complements webjs-doc-sync (which owns framework API/behaviour docs); this skill owns what the scaffold emits. +when_to_use: | + Examples that should trigger this skill: + "add a new demo to the feature gallery" + "the gallery should also ship in the saas template" + "update all three scaffold templates with this" + "add a backend-features showcase to the api template" + "make sure the scaffold teaches agents how to do X" + "did we keep the scaffold rule files in sync?" + "the saas home / theme / schema changed, sync everything" + finishing any change to what `webjs create` generates + Do NOT trigger for: a framework API/behaviour change with no scaffold + impact (use webjs-doc-sync), or a pure-internal refactor of the CLI + that does not change generated output. +--- + +# Keep every scaffold surface in sync with what `webjs create` generates + +The scaffold is the **primary way AI agents learn webjs**: an agent reads the +generated gallery/showcase and its comments to learn the idioms, then builds the +real app by adapting them. So a change to what `webjs create` emits is a +first-class change with MANY surfaces, and the recurring failure is updating one +(usually a template file) while the per-agent rule files, the scaffold tests, the +framework docs, and the preview apps drift behind. This skill closes that gap. + +It is the sibling of `webjs-doc-sync`. Division of labour: + +- **webjs-doc-sync** owns the framework's API/behaviour docs (the root `AGENTS.md` + API sections, `agent-docs/*.md`, docs-site topic pages, the marketing website). +- **webjs-scaffold-sync** (this skill) owns what the scaffold GENERATES and the + surfaces that DESCRIBE the scaffold. + +They overlap on two surfaces (the scaffold's per-agent rule files, and the +template matrix in the framework docs/README). Whichever skill reaches that +surface must update it; when in doubt run both. + +A hard commit gate enforces the FLOOR: `.claude/hooks/require-scaffold-with-src.sh` +BLOCKS a commit that stages framework-feature source (`packages/(core|server|cli)/src`) +with no scaffold surface (`packages/cli/templates` or `packages/cli/lib`) in the +same commit (escape hatch `WEBJS_NO_SCAFFOLD_GATE=1` for a change that genuinely +needs no scaffold update). The hook can only enforce "stage SOME scaffold +surface"; THIS skill does the substantive per-surface judgment and the +generate-boot-check verification. + +## The complete scaffold surface map + +Treat this as the universe. For any scaffold change, decide per surface whether +it applies, then update or consciously skip each. + +1. **The generators** (the code that writes the app): + - `packages/cli/lib/create.js` (the main generator: layout, home page, the + theme block, db/schema, the full-stack gallery wiring, the per-template + gates like `isApi` / `isSaas` / `!isApi`). + - `packages/cli/lib/saas-template.js` (the saas-only files: auth, login/signup, + dashboard, the saas schema). + - `packages/cli/lib/api-gallery.js` (the api backend-features showcase). + - Any future `*-template.js` / `*-gallery.js` split out for escaping sanity. +2. **The verbatim template files** copied into every app: + - `packages/cli/templates/gallery/**` (the UI feature gallery + example app, + shipped in full-stack AND saas). + - `packages/cli/templates/**` (everything else copied per app: `lib/utils/ui.ts`, + `public/`, `tsconfig.json`, `gitignore`, `.hooks/`, the metadata/route stubs). +3. **The per-agent rule files** (LOCKSTEP: all carry the SAME rules in each + agent's format, the #134/#136 divergence lesson). A convention/workflow change + for generated apps must land in ALL of them together: + - `packages/cli/templates/AGENTS.md` + - `packages/cli/templates/CLAUDE.md` + - `packages/cli/templates/CONVENTIONS.md` + - `packages/cli/templates/.cursorrules` + - `packages/cli/templates/.github/copilot-instructions.md` + - `packages/cli/templates/.agents/rules/workflow.md` + - `packages/cli/templates/.gemini/**`, `.opencode/**`, `.claude/**` (whatever + per-agent rule files the scaffold currently ships; enumerate, do not assume) +4. **The scaffold tests**: `test/scaffolds/*.test.js` (e.g. `scaffold-gallery`, + `scaffold-ui-integration`). A new demo/template/generated-file assertion goes + here, including the counterfactual (a per-template exclusion test). +5. **The framework docs that DESCRIBE the scaffold** (shared with doc-sync): + - Root `AGENTS.md` "Scaffolding" section. + - The docs site: `docs/app/docs/getting-started/page.ts` (+ `backend-only`, + `ai-first`, `conventions` where they describe generated structure). + - `README.md` (the template matrix + the "scaffold is the tutorial" note). +6. **The CLI surface**: `packages/cli/` `--template` validation + `--help`/usage + text when a template or flag is added or renamed. +7. **The preview / example / dogfood apps**: `examples/blog/` and any in-repo + apps, plus the local preview apps the user tests, when a convention they + demonstrate changes. + +## Change-type to surface mapping + +| Change | Surfaces that MUST be checked | +|---|---| +| New / changed **gallery or showcase demo** | the template file(s) or generator strings for the demo + the home-page `features`/index array + the scaffold AGENTS.md gallery list + `test/scaffolds/*` FEATURES/assertions + **generate + boot the affected template** | +| New / removed **template** | the `create.js` template branch (+ a `*-template.js` if large) + the "only N templates exist" list in EVERY per-agent rule file + the framework `AGENTS.md`/getting-started/README template matrix + the CLI `--template` validation + `--help` + `test/scaffolds/*` | +| New **convention/rule** for generated apps | ALL per-agent rule files in lockstep (surface 3) + repo `CONVENTIONS.md` if the repo demonstrates it + `agent-docs` only if it also changes a framework API | +| Changed **generated file** (layout, theme, home, schema, middleware) | the generator (`create.js`/`*-template.js`) + any scaffold test asserting it + any doc/preview describing it + **regenerate + boot** | +| New **scaffold-shipped config/hook** (`.hooks/`, `webjs.*` in the generated `package.json`, a check rule) | `templates/**` + `webjs doctor`/`check` that reads it + the per-agent rule files if agents must know it | +| Which **templates ship the gallery** (scoping) | the `copyGallery` / gallery gate in `create.js` + the "full-stack only / full-stack and saas" wording in the per-agent rule files AND the framework docs (grep the old scoping phrase everywhere) | +| Pure-internal CLI refactor (no change to generated output) | NONE. Record that no scaffold surface applies. | + +## Per-change sync procedure + +1. Identify the change's IDENTIFYING TOKENS: the demo route (`app/features/`, + `app/api/features/`), the template name, the generated file path, the + convention phrase, or the scoping phrase (e.g. "full-stack only"). +2. Grep those tokens across every scaffold surface: + ```sh + git grep -n -iE '|' -- \ + 'packages/cli/lib/**' 'packages/cli/templates/**' 'test/scaffolds/**' \ + AGENTS.md README.md 'docs/app/docs/**' + ``` +3. Update every surface the mapping says applies. For a SCOPING or wording change + (e.g. "gallery is full-stack only" becoming "full-stack and saas"), the grep + MUST surface and fix every copy, in the rule files AND the framework docs. +4. **VERIFY BY GENERATING (mandatory, non-negotiable).** The generators emit + strings, so a template-literal / escaping / interpolation bug is invisible in + the generator's own syntax and only appears in the GENERATED app. For each + affected template, generate an app and prove it: + ```sh + # generate (files only is enough for structure/typecheck; install to boot) + node -e "import('packages/cli/lib/create.js').then(m => m.scaffoldApp('probe', '/tmp/x', { template: 'saas', install: false }))" + # then in the generated app: webjs check (only no-scaffold-placeholder should + # remain), webjs typecheck (clean), and boot it to hit the new route(s). + ``` + A scaffold change is NOT done until a freshly generated app of each affected + template BOOTS, serves the new/changed route, passes `webjs check` (only the + intended `no-scaffold-placeholder` markers), and `webjs typecheck` is clean. +5. Run the scaffold tests (`node --test 'test/scaffolds/*.test.js'`) and add/adjust + assertions (a new demo in the FEATURES list, a per-template inclusion/exclusion + test, the counterfactual). +6. Respect the prose-punctuation invariant (#11) in every comment and doc, and + keep each demo densely commented (a header stating the webjs concept + the + why, inline comments on the non-obvious idiom, a `webjs-scaffold-placeholder` + marker). The scaffold teaches by its comments; a thin demo is a bug. + +## Audit-mode procedure (sweep the scaffold for drift) + +1. List the shipped scaffold changes (new demos, new template, scoping changes, + changed generated files). +2. For each, pull its tokens and run the surface grep above. +3. A surface is a GAP when the mapping says it applies but the token is absent or + describes stale behaviour there (a classic gap: a demo added to the full-stack + home but missing from the saas home, or a scoping phrase updated in one rule + file but not the other five). +4. For a bulk audit, file a grounded follow-up via **webjs-file-issue** per gap + (title `scaffold: missing `); for a single in-flight change, + just fix all surfaces on the same PR. + +## What this skill does NOT do + +- It does not regenerate `llms.txt` / `llms-full.txt` (live-generated) or the + website changelog (auto from PR titles). +- It does not own framework API/behaviour docs (that is `webjs-doc-sync`); when a + change touches both a framework API and the scaffold, run both skills. +- It does not decide whether a CLI change alters generated output; that judgement + is step 1, and a pure-internal refactor correctly updates no scaffold surface. diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md index 2c94cd5d0..10d0fc429 100644 --- a/.claude/skills/webjs-start-work/SKILL.md +++ b/.claude/skills/webjs-start-work/SKILL.md @@ -126,7 +126,7 @@ Doc drift is the #1 way a framework rots. Documentation MUST stay in sync with c - `CLAUDE.md` (only if a Claude Code rule is specifically added; framework conventions go in AGENTS.md). - `.github/*.md` (issue templates, PR templates, contributing) when a workflow rule shifts. 3. **User-facing docs site** under `docs/app/docs//page.ts` (these are `.ts` files, not markdown, so they're excluded by the markdown query but they're the canonical user-facing reference). If the change is visible to a user reading the docs site, update the matching topic page. Add a new page if the surface is new and there's no obvious home. -4. **Scaffold templates** under `packages/cli/templates/`. Update if the change affects what `webjs create` generates (default code, agent config files, `.hooks` content, scaffolded `package.json` shape). +4. **Scaffold templates** under `packages/cli/templates/` and the generators `packages/cli/lib/{create,saas-template,api-gallery}.js`. Update if the change affects what `webjs create` generates (default code, the gallery/showcase demos, agent config files, `.hooks` content, scaffolded `package.json` shape). The scaffold is webjs's PRIMARY teaching surface for AI agents, so this surface has many parts that must move in lockstep (the generators, the per-agent rule files, the scaffold tests, the framework docs that describe the scaffold, the preview/example apps) and a mandatory "generate + boot + `webjs check`" verification. **Invoke the `webjs-scaffold-sync` skill** to walk them all; it is the scaffold-side sibling of `webjs-doc-sync`. 5. **The MCP server** (the standalone `@webjsdev/mcp` package, `packages/mcp/src/{mcp,mcp-docs,mcp-source}.js`, extracted from the CLI in #415; `webjs mcp` and `npx @webjsdev/mcp` both run it). The MCP is how AI agents learn and introspect webjs, so it must stay in lockstep with the surfaces it exposes. Update it whenever the change touches what it serves: - **Introspection tools** (`list_routes` / `list_actions` / `list_components` / `check`): if you change the route table shape, the action/RPC-hash scheme, component registration, or a `webjs check` rule, update the matching tool projection so the MCP reports reality. - **Knowledge layer** (resources + `init` + `docs` + prompts): the resources are the `agent-docs/*.md` corpus + `AGENTS.md`, so a docs change is picked up automatically (it is bundled at `prepack`). But if you add or rename an `agent-docs` file, ADD A NEW INVARIANT, change the execution model, or add an authoring concept an agent should know, also: (a) confirm the `init` primer still pulls the right `AGENTS.md` sections (it sources the Execution-model + Invariants headings, so a heading rename breaks it), and (b) add a guided-workflow PROMPT for any new common recipe (a new page/route/action/component-shaped task). New recipes without a prompt are a silent gap. diff --git a/AGENTS.md b/AGENTS.md index e37a138ab..8f7128d9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,7 +67,8 @@ Every code change MUST include, automatically: 1. **Tests, every applicable layer (not just unit).** Ship the tests that prove the change across EVERY layer it touches: **unit** (`packages/*/test/**`, `test/**`, including the counterfactual that fails when reverted), **browser** (`*/test/**/browser/*` via `npm run test:browser`, for hydration / DOM / slots / client router / custom-element upgrade), **e2e** (`test/e2e/*.test.mjs` via `WEBJS_E2E=1`, including network probes / navigation / streaming), and **smoke** (`test/examples/*/smoke/*`). A unit test is NECESSARY BUT NOT SUFFICIENT for any client-router / component / browser-facing change (the headline behaviour is a browser/e2e assertion). **Bun parity is part of the task, not an afterthought:** webjs runs on Node 24+ AND Bun (#508), so a change to a runtime-sensitive surface (the serializer, the node:http vs `Bun.serve` listener + request path, SSR / action / CSRF dispatch, streams, `node:crypto`, the TS stripper, auth / session / cors) MUST be proven on Bun (`node scripts/run-bun-tests.js` + the touched `test/bun/*.mjs` under `bun`) AND ship an added/updated `test/bun/.mjs` cross-runtime assertion. `npm test` does NOT run browser, e2e, or Bun; run them yourself and report the result. Never report work done with failing or missing tests. See `agent-docs/testing.md`. Enforced by `.claude/hooks/require-tests-with-src.sh` (the scaffold variant WARNS unless `WEBJS_TEST_GATE=block`) and `.claude/hooks/require-bun-parity-with-runtime-src.sh` (BLOCKS a commit that stages runtime-sensitive source with no `test/bun/**` test; escape hatch `WEBJS_BUN_VERIFIED=1`). 2. **Documentation, part of the definition of done (not optional).** A task is NOT done until EVERY doc surface its change touches is in sync: `AGENTS.md` + `agent-docs/*.md` for new API surface, `CONVENTIONS.md` (and per-package `AGENTS.md`) for new conventions, the docs site (`docs/app/docs/`), the marketing `website/`, the scaffold templates (`packages/cli/templates/` per-agent rule files), and `README.md` for a headline capability. Updating `AGENTS.md` alone reproduces the #488 gap (docs site left stale). Invoke the `webjs-doc-sync` skill to sync every applicable surface. Enforced by `.claude/hooks/require-docs-with-src.sh`, which BLOCKS a commit that stages public `packages/*/src` source with no doc surface alongside it (a genuinely internal refactor / CI / release / perf change with no behaviour change bypasses with `WEBJS_NO_DOC_GATE=1`). -3. **Convention validation.** Run `webjs check` and fix violations. +3. **Scaffold sync, part of the definition of done (not optional).** The scaffold is webjs's PRIMARY teaching surface for AI agents: they learn the framework by reading the generated gallery/showcase and its comments, then build the real app by adapting them. So when a webjs FEATURE is added or changed, the scaffold that teaches it must move too, the same way the docs must. Ask on EVERY feature change: does a UI feature-gallery demo (`packages/cli/templates/gallery/`, shipped in full-stack + saas), an api backend-showcase endpoint (`packages/cli/lib/api-gallery.js`), a generator surface (`packages/cli/lib/{create,saas-template}.js` for the home/theme/schema/wiring), a scaffold convention (the per-agent rule files in lockstep), or a scaffold test (`test/scaffolds/**`) need to change? Invoke the `webjs-scaffold-sync` skill to walk every surface AND run its mandatory generate + boot + `webjs check` verification (the generators emit strings, so an escaping bug only shows in a freshly generated app). Enforced by `.claude/hooks/require-scaffold-with-src.sh`, which BLOCKS a commit that stages `packages/(core|server|cli)/src` feature source with no scaffold surface (`packages/cli/templates` or `packages/cli/lib`) alongside it (a bug fix, an internal refactor, or a tweak to an already-demoed feature that needs no scaffold change bypasses with `WEBJS_NO_SCAFFOLD_GATE=1`). +4. **Convention validation.** Run `webjs check` and fix violations. ### Git workflow (mandatory) @@ -416,7 +417,7 @@ const result = await optimistic(liked, true, () => likePost(postId)); ## Invariants (for both humans and agents) -> Hit one of these as a runtime error? The [Troubleshooting page](https://docs.webjs.com/docs/troubleshooting) is keyed by symptom (the throw-at-load server import, the backtick-in-template 500, the TypeScript strip failure, the SSR browser-global crash, the missing-frame swap) and maps each back to the invariant and the `webjs check` rule below. +> Hit one of these as a runtime error? The [Troubleshooting page](https://docs.webjs.dev/docs/troubleshooting) is keyed by symptom (the throw-at-load server import, the backtick-in-template 500, the TypeScript strip failure, the SSR browser-global crash, the missing-frame swap) and maps each back to the invariant and the `webjs check` rule below. 1. **Server-only code goes in `.server.{js,ts}` files, `route.ts` handlers, or `middleware.ts`. Never in pages, layouts, or components.** The `.server.{js,ts}` extension is the path-level boundary (the file router refuses to serve the source); a `'use server'` directive additionally makes exports RPC-callable, else the file is a server-only utility whose browser import is a throw-at-load stub. Importing a DB driver (`pg`), `node:*`, or any server-only dep from a component or an `app/**` page / layout / loading / error / not-found file crashes the browser at module load. 2. **Every `*.server.{js,ts}` file with `'use server'` exports must be `async` functions returning serializer-safe values.** Args and results round-trip via webjs's wire. Files without `'use server'` (server-only utilities) can export anything, including singletons. @@ -437,7 +438,7 @@ const result = await optimistic(liked, true, () => likePost(postId)); Three scaffolds exist (do not invent template names): `webjs create ` (full-stack: layout, page, components, modules, Drizzle+SQLite), `webjs create --template api` (backend-only routes + modules + Drizzle, no SSR), `webjs create --template saas` (auth + login/signup + protected dashboard + User model). The `--db sqlite|postgres` flag (default sqlite) picks the dialect; the schema/queries/actions are identical across dialects (see #563). The `--runtime node|bun` flag (default node, #541) is ORTHOGONAL to the template and re-flavors any of the three for Bun (`bun --bun` dev/start scripts so the SERVER runs on Bun, `bun.lock`, a pure `oven/bun:1` Dockerfile (#595; safe since cli@0.10.20's npx-free `webjs db migrate` (#570) needs no Node in the image) + bun-install CI, bun-command agent docs; the test/db/check tooling stays on Node); `bun create webjs ` auto-detects it. Pick from the request: default for any product with UI (todo, blog, dashboard, marketplace, social, e-commerce), `api` for an HTTP/JSON API with no UI, `saas` for accounts/login/signup; default to full-stack when ambiguous. -Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a real database (Drizzle + SQLite); NEVER use JSON files, in-memory arrays, or localStorage for persistence.** Update `db/schema.server.ts` to real models FIRST, then `webjs db generate` + `webjs db migrate`, then build pages/actions/queries. **Treat the scaffold as REFERENCE, not the final product:** replace the example page / `User` model / components and adapt `app/layout.ts` (brand, nav, content width; the default `
` reading column needs widening for a full-bleed app). ENFORCED: examples carry a `webjs-scaffold-placeholder` comment and `no-scaffold-placeholder` fails until the content is replaced and the marker deleted. Docs at https://docs.webjs.com. +Rules: **always scaffold via `webjs create`** (never hand-roll). **Default to a real database (Drizzle + SQLite); NEVER use JSON files, in-memory arrays, or localStorage for persistence.** Update `db/schema.server.ts` to real models FIRST, then `webjs db generate` + `webjs db migrate`, then build pages/actions/queries. **Treat the scaffold as REFERENCE, not the final product:** replace the example page / `User` model / components and adapt `app/layout.ts` (brand, nav, content width; the default `
` reading column needs widening for a full-bleed app). ENFORCED: examples carry a `webjs-scaffold-placeholder` comment and `no-scaffold-placeholder` fails until the content is replaced and the marker deleted. **The scaffold IS the primary teaching surface:** every UI scaffold (full-stack AND saas) ships a densely commented feature gallery (one webjs concept per demo under `app/features/`, logic in `modules/`, plus a whole example app under `app/examples/`), and the `api` template ships a backend-features showcase under `app/api/features/` (the `route()` adapter + validation, rate limiting, streaming, file storage, WebSockets + broadcast) listed in the root `app/route.ts` index; read the demos to learn the idioms, then prune per-feature. Docs at https://docs.webjs.dev. --- diff --git a/README.md b/README.md index 86b50f126..6e1d7dd7d 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,10 @@ TypeScript with zero build step, real SSR with Declarative Shadow DOM. > - Backend-only HTTP/JSON API, no UI → **`--template api`** > - Auth, login/signup, protected dashboard, SaaS → **`--template saas`** > +> **The scaffold is the tutorial.** Every UI scaffold (full-stack and saas) ships a commented **feature gallery** (`app/features/` + an example app under `app/examples/`); the `api` template ships a **backend-features showcase** (`app/api/features/`). Read the demos to learn the idioms, then prune per-feature. +> > Full rules: [`AGENTS.md` → How AI agents must scaffold](./AGENTS.md#how-ai-agents-must-scaffold). -> Full framework docs (every API, every recipe): **https://docs.webjs.com**. +> Full framework docs (every API, every recipe): **https://docs.webjs.dev**. ```sh # Get started in one command (no global install required) diff --git a/agent-docs/advanced.md b/agent-docs/advanced.md index c298d4144..291957d78 100644 --- a/agent-docs/advanced.md +++ b/agent-docs/advanced.md @@ -1246,7 +1246,7 @@ prod. A truncated stream (a server crash, a dropped connection) also throws rather than completing silently: a healthy stream always ends in an explicit terminal frame, so a missing one is an error. For a slow region you want behind a fallback on the FIRST paint, use `` instead; streaming RPC is for an imperative stream consumed -after an interaction. Full reference: the [Data fetching](https://docs.webjs.com/docs/data-fetching) page. +after an interaction. Full reference: the [Data fetching](https://docs.webjs.dev/docs/data-fetching) page. ### Opt out per link diff --git a/agent-docs/components.md b/agent-docs/components.md index 73f51c6e9..01ce6b95c 100644 --- a/agent-docs/components.md +++ b/agent-docs/components.md @@ -96,7 +96,7 @@ The SSR pipeline runs the **pre-render value-deriving hooks** before `render()`: For component-local state, create an instance signal in the constructor and call `signal.set(...)` to mutate. The built-in `SignalWatcher` re-runs `render()` on the next microtask; the same lifecycle hooks fire as for reactive-property changes. -See [`/docs/lifecycle`](https://docs.webjs.com/docs/lifecycle) for per-hook usage examples. +See [`/docs/lifecycle`](https://docs.webjs.dev/docs/lifecycle) for per-hook usage examples. ## Async render: bare-await data fetch (#469) diff --git a/docs/app/docs/ai-first/page.ts b/docs/app/docs/ai-first/page.ts index ab5bff62d..f405c261f 100644 --- a/docs/app/docs/ai-first/page.ts +++ b/docs/app/docs/ai-first/page.ts @@ -136,7 +136,7 @@ HTTP/JSON API only, no UI --template api Auth / login / signup / SaaS --template saas

The scaffold is REFERENCE, not the final product. The agent's job after scaffolding is to replace the example app/page.ts ("Hello from …"), the example User model in db/schema.server.ts, and the example components with the app the user actually requested. The infrastructure (Drizzle wiring, test config, agent rules, route conventions) stays.

-

When the scaffolded AGENTS.md doesn't cover what you need (an obscure directive, an auth-provider recipe, deployment specifics, edge cases), the full hosted documentation is at docs.webjs.com. Every API, every recipe, every example lives there. Reach for it before guessing or hand-rolling.

+

When the scaffolded AGENTS.md doesn't cover what you need (an obscure directive, an auth-provider recipe, deployment specifics, edge cases), the full hosted documentation is at docs.webjs.dev. Every API, every recipe, every example lives there. Reach for it before guessing or hand-rolling.

What an AI Agent Can Do with webjs

Given a webjs app + AGENTS.md, an AI coding assistant can:

diff --git a/docs/app/docs/getting-started/page.ts b/docs/app/docs/getting-started/page.ts index 352a9df25..466edbcfb 100644 --- a/docs/app/docs/getting-started/page.ts +++ b/docs/app/docs/getting-started/page.ts @@ -36,12 +36,14 @@ webjs create my-api --template api # SaaS starter (auth + dashboard + Drizzle User model + modules) webjs create my-app --template saas -

For AI agents: only those three templates exist, and the CLI rejects any other --template value. Default to full-stack with Drizzle + SQLite for any product app (todo, blog, dashboard, marketplace, social, e-commerce…). Pick --template api only if the user explicitly asks for a backend-only API with no UI. Pick --template saas only if the user explicitly asks for auth / login / accounts. The scaffold is a starting point. Replace the example layout, page, and User model with the app the user actually asked for. Use Drizzle for any persisted data, never JSON files, in-memory arrays, or localStorage. When AGENTS.md doesn't cover what you need, the full hosted docs are at docs.webjs.com.

+

For AI agents: only those three templates exist, and the CLI rejects any other --template value. Default to full-stack with Drizzle + SQLite for any product app (todo, blog, dashboard, marketplace, social, e-commerce…). Pick --template api only if the user explicitly asks for a backend-only API with no UI. Pick --template saas only if the user explicitly asks for auth / login / accounts. The scaffold is a starting point. Replace the example layout, page, and User model with the app the user actually asked for. Use Drizzle for any persisted data, never JSON files, in-memory arrays, or localStorage. When AGENTS.md doesn't cover what you need, the full hosted docs are at docs.webjs.dev.

The --template api scaffold generates thin route handlers that wrap typed server actions. Business logic lives in modules/. Routes just import and call the action/query, giving you file-based routing for URL structure plus type-safe server actions for logic.

The --template saas scaffold includes login + signup pages, a dashboard with auth middleware guard, settings page, auth API route, createAuth() with Credentials provider, Drizzle User model with password hashing, and a modules architecture (modules/auth/{actions,queries,types.ts}, db/connection.server.ts, lib/{auth,password}.ts).

+

The scaffold IS the tutorial. Every UI scaffold (full-stack AND saas) ships a densely commented feature gallery: one webjs concept per demo under app/features/ (routing, components, server actions, optimistic UI, async render, directives, forms, metadata, caching, env vars, the client router, service worker, WebSockets, broadcast, rate limiting, file storage, type-safe routes) with logic in modules/, plus a whole example app under app/examples/. The --template api scaffold ships the backend counterpart instead, a backend-features showcase under app/api/features/ (the route() adapter + input validation, rate limiting, a streaming response, file storage, and a WebSocket + broadcast endpoint) listed in the root app/route.ts index. Read each demo end to end (the code AND its comments) to learn the idioms, then prune the ones you do not need. Each demo carries a placeholder marker (the no-scaffold-placeholder check), so webjs check fails until you keep-and-adapt or delete it (the route AND its modules/<name>).

+

Scaffolding a Bun app

webjs runs on Node 24+ or Bun. To generate a Bun-flavored app, add --runtime bun (a separate axis from --template, so it works with all three). It is auto-detected when you scaffold through Bun, so both forms below produce the same Bun app:

# auto-detected: scaffolding through bun implies --runtime bun
diff --git a/docs/lib/llms.server.ts b/docs/lib/llms.server.ts
index 1bd67fdf0..4a3453b9f 100644
Binary files a/docs/lib/llms.server.ts and b/docs/lib/llms.server.ts differ
diff --git a/examples/blog/.agents/rules/workflow.md b/examples/blog/.agents/rules/workflow.md
index 666a38e27..e484fd146 100644
--- a/examples/blog/.agents/rules/workflow.md
+++ b/examples/blog/.agents/rules/workflow.md
@@ -3,7 +3,7 @@
 You are working on a webjs app, an AI-first, no-build, web-components-first
 framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for
 project-specific conventions before writing any code. When AGENTS.md does not
-cover what you need, the full hosted docs are at **https://docs.webjs.com**.
+cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
 
 ## Persistence + scaffold rules (non-negotiable)
 
diff --git a/examples/blog/.cursorrules b/examples/blog/.cursorrules
index f6207f99e..bf69dd129 100644
--- a/examples/blog/.cursorrules
+++ b/examples/blog/.cursorrules
@@ -3,7 +3,7 @@
 You are working on a webjs app, an AI-first, no-build, web-components-first
 framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for
 project-specific conventions before writing any code. When AGENTS.md doesn't
-cover what you need, the full hosted docs are at **https://docs.webjs.com**.
+cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
 
 ## Persistence + scaffold rules (non-negotiable)
 
diff --git a/examples/blog/.github/copilot-instructions.md b/examples/blog/.github/copilot-instructions.md
index 008513023..638924c33 100644
--- a/examples/blog/.github/copilot-instructions.md
+++ b/examples/blog/.github/copilot-instructions.md
@@ -3,7 +3,7 @@
 You are working on a webjs app, an AI-first, no-build, web-components-first
 framework. Read AGENTS.md for the full API reference and CONVENTIONS.md for
 project-specific conventions. When AGENTS.md doesn't cover what you need,
-the full hosted docs are at **https://docs.webjs.com**.
+the full hosted docs are at **https://docs.webjs.dev**.
 
 ## Persistence + scaffold rules (non-negotiable)
 
diff --git a/packages/cli/bin/webjs.js b/packages/cli/bin/webjs.js
index 05463c62a..dce29c138 100755
--- a/packages/cli/bin/webjs.js
+++ b/packages/cli/bin/webjs.js
@@ -524,7 +524,7 @@ components/schema with the actual app the user requested. Use Drizzle +
 SQLite for persistence (already wired up). Never store app data in JSON
 files.
 
-Full docs: https://docs.webjs.com`);
+Full docs: https://docs.webjs.dev`);
         process.exit(1);
       }
       const noInstall = rest.includes('--no-install');
diff --git a/packages/cli/lib/api-gallery.js b/packages/cli/lib/api-gallery.js
new file mode 100644
index 000000000..214753012
--- /dev/null
+++ b/packages/cli/lib/api-gallery.js
@@ -0,0 +1,229 @@
+/**
+ * Backend-features showcase for `webjs create --template api`.
+ * A set of JSON/HTTP endpoints under `app/api/features/` that demonstrate the
+ * backend capabilities an API app uses (the api counterpart of the UI gallery).
+ * Extracted here (like saas-template.js) to keep create.js readable and dodge
+ * nested-template-literal escaping: files are built from arrays of
+ * double-quoted strings, so `${...}` and backticks are emitted literally.
+ */
+
+import { mkdir, writeFile } from 'node:fs/promises';
+import { join } from 'node:path';
+
+/**
+ * Write the api backend-features gallery into `/app/api/features/**`
+ * plus a boot-time env-validation example at `app/env.ts`. Each demo carries a
+ * `webjs-scaffold-placeholder` marker so `webjs check` fails until it is
+ * pruned or adapted.
+ * @param {string} appDir
+ */
+export async function writeApiGallery(appDir) {
+  const feat = (...p) => join(appDir, 'app', 'api', 'features', ...p);
+
+  // 1) route() adapter + input validation.
+  await mkdir(join(appDir, 'modules', 'widgets', 'actions'), { recursive: true });
+  await writeFile(join(appDir, 'modules', 'widgets', 'actions', 'create-widget.server.ts'), [
+    "'use server';",
+    "",
+    "// A plain 'use server' mutation. Exposed over REST by the route() adapter in",
+    "// app/api/features/validate/route.ts, which merges the query + params + JSON",
+    "// body into one input, runs the boundary validator, and JSON-responds.",
+    "export async function createWidget(input: { name: string }) {",
+    "  return { id: crypto.randomUUID(), name: input.name, createdAt: new Date().toISOString() };",
+    "}",
+    "",
+  ].join('\n'));
+  await mkdir(feat('validate'), { recursive: true });
+  await writeFile(feat('validate', 'route.ts'), [
+    "// webjs-scaffold-placeholder. API backend-features demo. Keep and adapt it, or prune it (delete this app/api/features/validate route AND modules/widgets), then delete this marker line. webjs check fails while the marker remains.",
+    "// route() turns a 'use server' action into a REST endpoint: it merges the URL",
+    "// query, route params, and JSON body into one input, runs `validate` at the",
+    "// boundary (a { success:false, fieldErrors } return is a 422, no action call),",
+    "// then JSON-responds the result. POST { \"name\": \"Gadget\" } to try it.",
+    "import { route } from '@webjsdev/server';",
+    "import { createWidget } from '#modules/widgets/actions/create-widget.server.ts';",
+    "",
+    "const validate = (input: { name?: unknown }) => {",
+    "  const name = typeof input?.name === 'string' ? input.name.trim() : '';",
+    "  if (!name) return { success: false as const, fieldErrors: { name: 'name is required' } };",
+    "  return { success: true as const, data: { name } };",
+    "};",
+    "",
+    "// A GET returns usage so the endpoint is explorable in a browser; the real",
+    "// validated action is the POST below.",
+    "export async function GET(req: Request) {",
+    "  const base = new URL(req.url).origin;",
+    "  return Response.json({",
+    "    method: 'POST',",
+    "    usage: 'POST JSON { name } to validate the input and create a widget; an empty name returns a 422 with fieldErrors.',",
+    "    example: 'curl -X POST -H content-type:application/json -d {\"name\":\"Gadget\"} ' + base + '/api/features/validate',",
+    "  });",
+    "}",
+    "",
+    "export const POST = route(createWidget, { validate });",
+    "",
+  ].join('\n'));
+
+  // 2) Rate limiting (middleware scoped to this endpoint).
+  await mkdir(feat('rate-limit'), { recursive: true });
+  await writeFile(feat('rate-limit', 'middleware.ts'), [
+    "// Per-segment middleware: it sits beside this route, so it rate-limits ONLY",
+    "// /api/features/rate-limit. rateLimit() is backed by the pluggable cache store",
+    "// (in-memory by default; point it at Redis to share the window across nodes).",
+    "import { rateLimit } from '@webjsdev/server';",
+    "",
+    "export default rateLimit({ window: '10s', max: 5, message: 'Slow down: five requests per ten seconds.' });",
+    "",
+  ].join('\n'));
+  await writeFile(feat('rate-limit', 'route.ts'), [
+    "// webjs-scaffold-placeholder. API backend-features demo. Keep and adapt it, or prune it (delete this app/api/features/rate-limit route), then delete this marker line. webjs check fails while the marker remains.",
+    "// The middleware.ts beside this file stamps X-RateLimit-* headers and returns",
+    "// a 429 with Retry-After once the window is exhausted, so this handler never",
+    "// runs on a limited request. Call it six times in ten seconds to see the 429.",
+    "export async function GET() {",
+    "  return Response.json({ ok: true, at: new Date().toISOString() });",
+    "}",
+    "",
+  ].join('\n'));
+
+  // 3) Streaming response (chunks flushed as produced, no buffering).
+  await mkdir(feat('stream'), { recursive: true });
+  await writeFile(feat('stream', 'route.ts'), [
+    "// webjs-scaffold-placeholder. API backend-features demo. Keep and adapt it, or prune it (delete this app/api/features/stream route), then delete this marker line. webjs check fails while the marker remains.",
+    "// Streams JSON-per-line chunks as they are produced, so a slow or large",
+    "// result is delivered incrementally instead of buffered whole. A hand-written",
+    "// route.ts returns a ReadableStream for full control. Served as text/plain so",
+    "// it renders INLINE and incrementally in a browser (application/x-ndjson would",
+    "// make the browser download a file instead); `curl -N` shows the same lines",
+    "// arrive one at a time.",
+    "export async function GET() {",
+    "  const encoder = new TextEncoder();",
+    "  const stream = new ReadableStream({",
+    "    async start(controller) {",
+    "      for (let i = 1; i <= 5; i++) {",
+    "        controller.enqueue(encoder.encode(JSON.stringify({ n: i, at: new Date().toISOString() }) + '\\n'));",
+    "        await new Promise((r) => setTimeout(r, 200));",
+    "      }",
+    "      controller.close();",
+    "    },",
+    "  });",
+    "  return new Response(stream, { headers: { 'content-type': 'text/plain; charset=utf-8', 'x-content-type-options': 'nosniff' } });",
+    "}",
+    "",
+  ].join('\n'));
+
+  // 4) File storage (upload + serve), both hand-written route.ts.
+  await mkdir(feat('files', '[key]'), { recursive: true });
+  await writeFile(feat('files', 'route.ts'), [
+    "// webjs-scaffold-placeholder. API backend-features demo. Keep and adapt it, or prune it (delete this app/api/features/files route), then delete this marker line. webjs check fails while the marker remains.",
+    "// POST a multipart file: the bytes stream into the FileStore (a local",
+    "// .webjs/uploads dir by default, gitignored; swap for S3/R2 with one",
+    "// setFileStore() call). Returns the key + a URL served by [key]/route.ts.",
+    "// Try: curl -F file=@README.md http://localhost:8080/api/features/files",
+    "import { getFileStore, generateKey } from '@webjsdev/server';",
+    "",
+    "// A GET on the upload endpoint returns usage, so it is explorable in a browser",
+    "// (the real upload is the POST below).",
+    "export async function GET(req: Request) {",
+    "  const base = new URL(req.url).origin;",
+    "  return Response.json({",
+    "    method: 'POST',",
+    "    usage: 'POST a multipart form with a `file` field to upload.',",
+    "    example: 'curl -F file=@README.md ' + base + '/api/features/files',",
+    "    serve: base + '/api/features/files/',",
+    "  });",
+    "}",
+    "",
+    "export async function POST(req: Request) {",
+    "  const form = await req.formData();",
+    "  const file = form.get('file');",
+    "  if (!(file instanceof File) || file.size === 0) {",
+    "    return Response.json({ error: 'no file provided' }, { status: 400 });",
+    "  }",
+    "  const key = generateKey(file.name);",
+    "  const { size, contentType } = await getFileStore().put(key, file, { contentType: file.type });",
+    "  const base = new URL(req.url).origin;",
+    "  return Response.json({ key, size, contentType, url: `${base}/api/features/files/${key}` });",
+    "}",
+    "",
+  ].join('\n'));
+  await writeFile(feat('files', '[key]', 'route.ts'), [
+    "// Serves a stored file back by key. getFileStore().get(key) returns the bytes",
+    "// as a web ReadableStream (streamed, never buffered whole) plus the recorded",
+    "// content type. The key is validated inside the store (traversal-safe).",
+    "import { getFileStore } from '@webjsdev/server';",
+    "",
+    "export async function GET(_req: Request, { params }: { params: { key: string } }) {",
+    "  const file = await getFileStore().get(params.key);",
+    "  if (!file) return new Response('Not found', { status: 404 });",
+    "  return new Response(file.body as ReadableStream, {",
+    "    headers: { 'content-type': file.contentType, 'content-length': String(file.size) },",
+    "  });",
+    "}",
+    "",
+  ].join('\n'));
+
+  // 5) WebSocket endpoint with broadcast fan-out.
+  await mkdir(feat('ws'), { recursive: true });
+  await writeFile(feat('ws', 'route.ts'), [
+    "// webjs-scaffold-placeholder. API backend-features demo. Keep and adapt it, or prune it (delete this app/api/features/ws route), then delete this marker line. webjs check fails while the marker remains.",
+    "// A WebSocket endpoint: exporting WS(ws, req) upgrades this route to a socket.",
+    "// The framework auto-registers each connection to its path, so broadcast()",
+    "// fans a message out to every connected client. Connect two clients to",
+    "// ws://localhost:8080/api/features/ws and watch messages arrive in both.",
+    "import { broadcast } from '@webjsdev/server';",
+    "",
+    "// A plain GET (a browser opening the URL) returns usage; the live socket is",
+    "// the WS handler below, reached over ws:// with a WebSocket client.",
+    "export async function GET(req: Request) {",
+    "  const base = new URL(req.url).origin.replace(/^http/, 'ws');",
+    "  return Response.json({",
+    "    protocol: 'WebSocket',",
+    "    usage: 'Connect over ws:// and send a message; it is broadcast to every connected client.',",
+    "    url: base + '/api/features/ws',",
+    "  });",
+    "}",
+    "",
+    "type WSLike = {",
+    "  on(event: 'message' | 'close', cb: (data: Buffer) => void): void;",
+    "  send(msg: string): void;",
+    "};",
+    "",
+    "export function WS(ws: WSLike) {",
+    "  ws.on('message', (data) => {",
+    "    broadcast('/api/features/ws', data.toString());",
+    "  });",
+    "}",
+    "",
+  ].join('\n'));
+
+  // 6) Boot-time env validation (opt-in). env.ts lives at the app ROOT (sibling
+  // to middleware.ts), not inside app/. All-optional so it never blocks boot.
+  await writeFile(join(appDir, 'env.ts'), [
+    "// Boot-time env validation (opt-in, app-root env.ts). A schema of var name to",
+    "// type/options; webjs coerces values, applies defaults, and fails fast naming",
+    "// EVERY bad var. Kept all-optional here so it never blocks a fresh boot; make",
+    "// a var required by dropping `optional`.",
+    "export default {",
+    "  DATABASE_URL: { type: 'string', optional: true },",
+    "  PORT: { type: 'number', optional: true, default: 8080 },",
+    "  NODE_ENV: { type: 'enum', values: ['development', 'production', 'test'], optional: true },",
+    "};",
+    "",
+  ].join('\n'));
+
+  // A starter test for the validated action.
+  await writeFile(join(appDir, 'test', 'unit', 'widgets.test.ts'), [
+    "import { test } from 'node:test';",
+    "import assert from 'node:assert/strict';",
+    "",
+    "import { createWidget } from '#modules/widgets/actions/create-widget.server.ts';",
+    "",
+    "test('createWidget echoes the name and mints an id', async () => {",
+    "  const w = await createWidget({ name: 'Gadget' });",
+    "  assert.equal(w.name, 'Gadget');",
+    "  assert.ok(w.id);",
+    "});",
+    "",
+  ].join('\n'));
+}
diff --git a/packages/cli/lib/create.js b/packages/cli/lib/create.js
index 7ae08c9ff..eb617a1df 100644
--- a/packages/cli/lib/create.js
+++ b/packages/cli/lib/create.js
@@ -134,6 +134,28 @@ async function copyUiComponents(appDir, names) {
   }
 }
 
+/**
+ * Copy the example gallery (idiomatic, densely-commented working examples) into
+ * the scaffolded app. Merges `templates/gallery/{app,modules}` over the app so
+ * single-feature demos land under `app/features//`, whole example apps
+ * under `app/examples//`, and their logic under `modules//`, the
+ * app-thin + modules-logic split webjs prescribes.
+ *
+ * Ships verbatim (no `{{APP_NAME}}` substitution): the examples are self-
+ * contained and reference only `@webjsdev/*`, drizzle, `#db/*`, and each other.
+ * The scaffold's own `app/page.ts` / `app/layout.ts` are written AFTER this and
+ * the gallery ships neither, so there is no clobber. `cp` merges into existing
+ * `app/` and `modules/` dirs rather than replacing them.
+ *
+ * @param {string} appDir
+ */
+async function copyGallery(appDir) {
+  const galleryDir = join(TEMPLATES, 'gallery');
+  for (const sub of ['app', 'modules']) {
+    await cp(join(galleryDir, sub), join(appDir, sub), { recursive: true });
+  }
+}
+
 /**
  * Write `lib/utils/cn.ts` (the `cn()` helper) and `components.json` so the
  * scaffolded app is pre-initialised for `webjs ui add`. Reads the registry's
@@ -165,11 +187,13 @@ async function writeUiBootstrap(appDir) {
   // 2) components.json: the same shape `webjsui init` writes for webjs
   // projects (see packages/ui/src/utils/detect-project.js). The utils alias
   // is lib/utils/cn so get-config.js's `+ '.ts'` resolves to lib/utils/cn.ts.
+  // The theme CSS lives at styles/globals.css, NOT app/globals.css: app/ is
+  // routing-only, so a non-routing stylesheet does not belong there.
   const componentsJson = {
     $schema: 'https://ui.webjs.dev/schema.json',
     style: 'default',
     tailwind: {
-      css: 'app/globals.css',
+      css: 'styles/globals.css',
       baseColor: 'neutral',
       cssVariables: true,
     },
@@ -186,17 +210,20 @@ async function writeUiBootstrap(appDir) {
     JSON.stringify(componentsJson, null, 2) + '\n',
   );
 
-  // 3) app/globals.css: copy the neutral theme verbatim. components.json
-  // references this path, and future `webjs ui add` calls append to it.
+  // 3) styles/globals.css: copy the neutral theme verbatim. components.json
+  // references this path, and future `webjs ui add` calls append to it. It
+  // lives OUTSIDE app/ because app/ is routing-only; the layout inlines the
+  // same tokens into a 
     
     
 
-    
- - ${name} +
+ + ${displayName}