You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The full-stack scaffold gallery (packages/cli/templates/gallery/) never teaches
HTTP-verb server actions (#488). A freshly scaffolded app browsing /features/server-actions learns the 'use server' vs server-only-.server.ts
boundary, export const middleware, actionContext(), and actionSignal(), but
nothing about the reserved sibling config exports that decide an action's
transport and caching: method, cache, tags, invalidates.
What the gallery ships today:
export const method = 'GET' appears twice, as incidental usage with a short
comment, in modules/todo/queries/list-todos.server.ts:9 and modules/async-render/queries/server-greeting.server.ts:5. Neither is on a card
that explains the idiom, so an agent reading the gallery for the pattern finds it
only by accident.
export const cache, tags, and invalidates appear NOWHERE as working code in
the UI gallery. The caching card (app/features/caching/page.ts:36) namechecks
"or a GET action's export const cache" in a trailing sentence and demonstrates
nothing; that card is entirely about the page-level export const revalidate + revalidatePath path.
The api template does teach it (packages/cli/lib/create.js:946-975: method = 'GET' + cache = 30 + tags, paired with invalidates on the
mutation), so the full-stack app, the one most agents scaffold, is the surface
missing it.
#576 adopted the verb idiom across the monorepo apps and the templates' modules/users
code, but the gallery card layer was not extended with it, so the teaching gap
survived the adoption.
Design / approach
Extend the EXISTING server-actions card rather than adding a new one. Verbs are a
property of a server action, not a separate feature, and the card is already the
place where the action's config exports (middleware) are explained, so this is a
natural second section on the same card. Adding a /features/http-verbs card would
split one concept across two routes and grow the prune surface for an app that does
not need it.
The card should teach, with runnable code the visitor can trigger:
export const method = 'GET' makes args ride the URL, is CSRF-exempt, carries Cache-Control + a weak ETag (304 on If-None-Match), and is SSR-seeded so the
component does not re-fetch on hydration. Absent method means POST.
export const cache = N (or { maxAge, swr }) is the response cache window, private by default. Call out the safety rule explicitly: { public: true }
shares the response across users keyed only by URL + args, so it is only for data
identical for every visitor, the same rule as a page's export const revalidate.
export const tags labels a GET's cached entry; export const invalidates on the
mutation evicts those tags on success, so the next read refetches instead of
serving a stale browser-cached value (X-Webjs-Invalidate).
A good demo shape: a cached GET read whose value visibly freezes for its window
(a timestamp or counter), plus a mutation button declaring invalidates for the
same tag, so clicking it makes the next read change immediately. That is the same
before/after affordance the caching card already uses with revalidatePath, which
keeps the two cards consistent and lets the caching card's dangling sentence link
here instead of trailing off.
packages/cli/templates/gallery/modules/server-actions/: the demo code lives
here, alongside actions/greet.server.ts, middleware/require-auth.server.ts, utils/format.server.ts, components/greeter.ts. One function per configured file is a hard rule: a file carrying method / cache / tags / invalidates / validate / middleware with more than one
callable export is a webjs check error. So the cached GET goes in its own queries/<name>.server.ts and the invalidating mutation in its own actions/<name>.server.ts. greet.server.ts already models the pattern of
keeping a private non-exported helper in the file to stay within the rule.
packages/cli/templates/gallery/modules/gallery/nav.ts:30: the card blurb still
reads "A use-server RPC action next to a server-only .server.ts utility, and why
the boundary matters." Update it to mention verbs / caching, or the index will
under-sell what the card now teaches.
packages/cli/templates/gallery/app/features/caching/page.ts:33-38: replace the
dangling "or a GET action's export const cache" sentence with a real link to /features/server-actions, so the two cards cross-reference instead of leaving a
dead end.
Landmines / gotchas:
The card must survive gallery:clear pruning as a unit. The server-actions
card already depends on the auth card (the greeter 401s when signed out, and the
page says "prune both together"). Do not add a third cross-card dependency; keep
the new demo self-contained inside modules/server-actions/.
cache is private by default and the demo must stay that way. Do not write { public: true } in scaffold code an agent will copy: the gallery is a teaching
surface and a copied public: true on a per-user read is a real cross-user data
leak. Teach the flag by naming it and the rule, not by shipping it.
A streamed result is never cached / ETagged / seeded, so do not combine the
new cached-GET demo with the streaming-action demo.
A GET's args ride the URL with a 4KB cap (over that it falls back to POST);
keep the demo's args small so the demonstrated transport is the one described.
Prose in scaffold templates is subject to the repo's banned-glyph rules (no
em-dashes, no space-surrounded hyphen or semicolon as a pause, WebJs capitalized
in prose): .claude/hooks/block-prose-punctuation.sh blocks a violating edit.
Invariants to respect:
Server-only code stays in .server.{js,ts}; the demo component imports the action
through the normal import (the RPC stub), never a hand-written fetch()
(AGENTS.md invariant 1 + the server-actions section).
Gallery pages are pages, so they never hydrate: all interactivity belongs in the
demo component under modules/server-actions/components/.
Tests + docs surfaces:
test/scaffolds/scaffold-gallery.test.js asserts every app/features/<name> page
exists, is listed in the nav, and carries no placeholder marker. A new module file
under an already-listed route needs no new entry, but if the demo adds a route,
update FEATURES (L29) and MODULE_ROUTES (L39).
test/scaffolds/gallery-coverage.json gates @webjsdev/core / @webjsdev/server
EXPORTS and routing conventions. Reserved action config exports (method, cache, tags, invalidates) are not module exports, so nothing in the manifest would
have caught this gap. Consider whether the manifest (or a sibling assertion in scaffold-gallery.test.js) should assert the gallery demonstrates each reserved
config export, which is what would keep this from regressing.
Verify by generating an app and booting it (webjs create + webjs dev + webjs check), per the webjs-scaffold-sync skill: the generators emit strings, so
an escaping bug only shows in a freshly generated app.
/features/server-actions in a freshly generated full-stack app teaches export const method, cache, tags, and invalidates, with the
private-by-default safety rule stated
The card ships a runnable demo where a cached GET visibly freezes for its
window and an invalidates mutation makes the next read change immediately
Each configured action file holds exactly one callable function, and webjs check passes on the generated app
The gallery nav blurb (modules/gallery/nav.ts:30) reflects the expanded card
The caching card links to the server-actions card instead of dangling on
"or a GET action's export const cache"
Generate + boot + webjs check verified on a fresh app, not just in-repo
Gallery tests cover the new files; a counterfactual proves the assertion fires
Consider a regression guard so a future reserved config export cannot go
undemonstrated silently
Problem
The full-stack scaffold gallery (
packages/cli/templates/gallery/) never teachesHTTP-verb server actions (#488). A freshly scaffolded app browsing
/features/server-actionslearns the'use server'vs server-only-.server.tsboundary,
export const middleware,actionContext(), andactionSignal(), butnothing about the reserved sibling config exports that decide an action's
transport and caching:
method,cache,tags,invalidates.What the gallery ships today:
export const method = 'GET'appears twice, as incidental usage with a shortcomment, in
modules/todo/queries/list-todos.server.ts:9andmodules/async-render/queries/server-greeting.server.ts:5. Neither is on a cardthat explains the idiom, so an agent reading the gallery for the pattern finds it
only by accident.
export const cache,tags, andinvalidatesappear NOWHERE as working code inthe UI gallery. The caching card (
app/features/caching/page.ts:36) namechecks"or a GET action's
export const cache" in a trailing sentence and demonstratesnothing; that card is entirely about the page-level
export const revalidate+revalidatePathpath.packages/cli/lib/create.js:946-975:method = 'GET'+cache = 30+tags, paired withinvalidateson themutation), so the full-stack app, the one most agents scaffold, is the surface
missing it.
#576 adopted the verb idiom across the monorepo apps and the templates'
modules/userscode, but the gallery card layer was not extended with it, so the teaching gap
survived the adoption.
Design / approach
Extend the EXISTING server-actions card rather than adding a new one. Verbs are a
property of a server action, not a separate feature, and the card is already the
place where the action's config exports (
middleware) are explained, so this is anatural second section on the same card. Adding a
/features/http-verbscard wouldsplit one concept across two routes and grow the prune surface for an app that does
not need it.
The card should teach, with runnable code the visitor can trigger:
export const method = 'GET'makes args ride the URL, is CSRF-exempt, carriesCache-Control+ a weak ETag (304 onIf-None-Match), and is SSR-seeded so thecomponent does not re-fetch on hydration. Absent
methodmeans POST.export const cache = N(or{ maxAge, swr }) is the response cache window,privateby default. Call out the safety rule explicitly:{ public: true }shares the response across users keyed only by URL + args, so it is only for data
identical for every visitor, the same rule as a page's
export const revalidate.export const tagslabels a GET's cached entry;export const invalidateson themutation evicts those tags on success, so the next read refetches instead of
serving a stale browser-cached value (
X-Webjs-Invalidate).A good demo shape: a cached GET read whose value visibly freezes for its window
(a timestamp or counter), plus a mutation button declaring
invalidatesfor thesame tag, so clicking it makes the next read change immediately. That is the same
before/after affordance the caching card already uses with
revalidatePath, whichkeeps the two cards consistent and lets the caching card's dangling sentence link
here instead of trailing off.
Implementation notes (for the implementing agent)
Where to edit:
packages/cli/templates/gallery/app/features/server-actions/page.ts: the cardcopy. It currently covers only the middleware /
actionContext/actionSignalstory (added by Scaffold gallery: add a <webjs-frame> demo and rework the server-actions demo to show middleware/actionContext/actionSignal #988). Add the verbs + caching section and mount the new demo
component.
packages/cli/templates/gallery/modules/server-actions/: the demo code liveshere, alongside
actions/greet.server.ts,middleware/require-auth.server.ts,utils/format.server.ts,components/greeter.ts.One function per configured file is a hard rule: a file carrying
method/cache/tags/invalidates/validate/middlewarewith more than onecallable export is a
webjs checkerror. So the cached GET goes in its ownqueries/<name>.server.tsand the invalidating mutation in its ownactions/<name>.server.ts.greet.server.tsalready models the pattern ofkeeping a private non-exported helper in the file to stay within the rule.
packages/cli/templates/gallery/modules/gallery/nav.ts:30: the card blurb stillreads "A use-server RPC action next to a server-only .server.ts utility, and why
the boundary matters." Update it to mention verbs / caching, or the index will
under-sell what the card now teaches.
packages/cli/templates/gallery/app/features/caching/page.ts:33-38: replace thedangling "or a GET action's
export const cache" sentence with a real link to/features/server-actions, so the two cards cross-reference instead of leaving adead end.
Landmines / gotchas:
gallery:clearpruning as a unit. The server-actionscard already depends on the auth card (the greeter 401s when signed out, and the
page says "prune both together"). Do not add a third cross-card dependency; keep
the new demo self-contained inside
modules/server-actions/.cacheisprivateby default and the demo must stay that way. Do not write{ public: true }in scaffold code an agent will copy: the gallery is a teachingsurface and a copied
public: trueon a per-user read is a real cross-user dataleak. Teach the flag by naming it and the rule, not by shipping it.
new cached-GET demo with the streaming-action demo.
keep the demo's args small so the demonstrated transport is the one described.
during SSR consumes the seed on its first client call, so a naive "watch the
network tab" instruction is wrong for the first paint. Either say so in the copy
or drive the demo from an explicit user-triggered refetch.
em-dashes, no space-surrounded hyphen or semicolon as a pause,
WebJscapitalizedin prose):
.claude/hooks/block-prose-punctuation.shblocks a violating edit.Invariants to respect:
.server.{js,ts}; the demo component imports the actionthrough the normal import (the RPC stub), never a hand-written
fetch()(AGENTS.md invariant 1 + the server-actions section).
#components/ui/*class helpers(
cardClass(),buttonClass()), matching the rest of the gallery after Refactor the scaffold gallery onto a @webjsdev/ui class-helper design system #1059.demo component under
modules/server-actions/components/.Tests + docs surfaces:
test/scaffolds/scaffold-gallery.test.jsasserts everyapp/features/<name>pageexists, is listed in the nav, and carries no placeholder marker. A new module file
under an already-listed route needs no new entry, but if the demo adds a route,
update
FEATURES(L29) andMODULE_ROUTES(L39).test/scaffolds/gallery-coverage.jsongates@webjsdev/core/@webjsdev/serverEXPORTS and routing conventions. Reserved action config exports (
method,cache,tags,invalidates) are not module exports, so nothing in the manifest wouldhave caught this gap. Consider whether the manifest (or a sibling assertion in
scaffold-gallery.test.js) should assert the gallery demonstrates each reservedconfig export, which is what would keep this from regressing.
webjs create+webjs dev+webjs check), per the webjs-scaffold-sync skill: the generators emit strings, soan escaping bug only shows in a freshly generated app.
is enumerated. The docs site already covers verbs (docs: server-actions page missing HTTP-verb surface (#488-#492) #572) and the
swroption(docs: explain the swr cache option on GET actions; README omits cached GET reads #1148), so this is scaffold-side only unless the card introduces new phrasing
worth mirroring.
Acceptance criteria
/features/server-actionsin a freshly generated full-stack app teachesexport const method,cache,tags, andinvalidates, with theprivate-by-default safety rule stated
window and an
invalidatesmutation makes the next read change immediatelywebjs checkpasses on the generated appmodules/gallery/nav.ts:30) reflects the expanded card"or a GET action's
export const cache"webjs checkverified on a fresh app, not just in-repoundemonstrated silently