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
HTTP-verb server actions (#488) let a 'use server' action declare its transport semantics via reserved sibling exports (export const method = 'GET', cache, tags, invalidates, validate, middleware). Reads become cacheable, ETag/304-aware, SSR-seeded GETs; mutations declare the tags they evict. The feature shipped in #494 and is documented, but our own dogfood surface barely uses it:
examples/blog has a synthetic modules/verbdemo/ demo (GET getGreeting/getSlow, mutation bumpGreeting, streaming streamTokens), but its REAL domain code (modules/posts, modules/comments, modules/auth queries) is all POST-default. Reads that should be cached GETs are not.
websitemodules/blog/queries/* and modules/changelog/queries/* are plain POST-default 'use server' reads, ideal GET + cache + tags candidates.
docs and packages/ui/packages/website have no server actions (nothing to convert; verify and skip).
All 3 scaffold templates ship ZERO HTTP-verb actions. The full-stack/api modules/users/queries/list-users.server.ts read and actions/create-user.server.ts mutation (in packages/cli/lib/create.js ~L777/L787) are POST-default; saas modules/auth/queries/current-user.server.ts (in packages/cli/lib/saas-template.js ~L146) is a POST-default read. New apps therefore never see the idiom.
This is a dogfooding + teaching gap: the framework's flagship reads/writes don't model the verb idiom we tell users to adopt.
Design / approach
Convert genuine reads to export const method = 'GET' with a sensible cache window and tags, and annotate mutations with invalidates for the matching tags, so the browser-cache coordinator revalidates after a write. Follow the reference idioms already in examples/blog/modules/verbdemo/ (get-greeting.server.ts for a cached/tagged GET, bump-greeting.server.ts for an invalidating mutation).
SAFETY: cache is private by default; only set { public: true } (or a page-style shared cache) on data identical for every visitor (public blog/changelog reads qualify; per-user/session reads like current-user do NOT, keep them private and short or uncached). A GET action must be idempotent.
Scope: convert where it is genuinely correct, do not force verbs onto write-shaped or per-user actions. Keep one function per file (a configured file with >1 callable fn is a webjs check error).
packages/cli/lib/saas-template.js: modules/auth/queries/current-user.server.ts (~L146) is per-session, leave POST-default (good teaching counter-example); consider a brief comment noting why it is NOT a public GET.
Landmines:
One function per file is enforced; do not co-locate the config exports with a second fn.
public: true on a per-user read is a data-leak; reviewers will reject it. Default private.
The scaffold users query returns hard-coded sample data with a webjs-scaffold-placeholder marker; keep the marker/TODO, only add the verb config exports.
Bun-mode scaffold is DERIVED from the canonical node template via runtime-rewrite.js transforms; editing the canonical strings in create.js/saas-template.js covers both runtimes (no parallel bun template).
Other AI agents are active in this repo; keep the change tightly scoped to action/query files + their tests/docs and rebase before merge.
Scaffold: test/scaffolds/scaffold-integration.test.js (assert the generated users query declares method = 'GET' and the action declares invalidates); counterfactual: the assertion fails before the create.js edit.
Apps: app-level smoke/e2e where present (test/examples/*), assert a converted GET responds with Cache-Control/ETag.
Docs: update scaffold-facing templates/CONVENTIONS.md/AGENTS.md if they describe the example modules; run the webjs-doc-sync skill to catch every surface. Run webjs check and fix violations.
Acceptance criteria
website public blog/changelog reads are GET + cached + tagged
examples/blog real-domain reads are GET + tagged and mutations declare invalidates (verbdemo already covers the standalone demo)
docs + ui-website confirmed to have no convertible actions
all 3 scaffolds emit at least one GET query (with cache+tags) and one mutation with invalidates; new apps model the idiom out of the box
webjs check passes; one-function-per-file respected; no public: true on per-user reads
scaffold-integration test asserts the new verb config exports, with a counterfactual that fails when reverted
docs/AGENTS/CONVENTIONS surfaces synced for any changed example modules
Problem
HTTP-verb server actions (#488) let a
'use server'action declare its transport semantics via reserved sibling exports (export const method = 'GET',cache,tags,invalidates,validate,middleware). Reads become cacheable, ETag/304-aware, SSR-seeded GETs; mutations declare the tags they evict. The feature shipped in #494 and is documented, but our own dogfood surface barely uses it:modules/verbdemo/demo (GETgetGreeting/getSlow, mutationbumpGreeting, streamingstreamTokens), but its REAL domain code (modules/posts,modules/comments,modules/authqueries) is all POST-default. Reads that should be cached GETs are not.modules/blog/queries/*andmodules/changelog/queries/*are plain POST-default'use server'reads, ideal GET +cache+tagscandidates.modules/users/queries/list-users.server.tsread andactions/create-user.server.tsmutation (inpackages/cli/lib/create.js~L777/L787) are POST-default; saasmodules/auth/queries/current-user.server.ts(inpackages/cli/lib/saas-template.js~L146) is a POST-default read. New apps therefore never see the idiom.This is a dogfooding + teaching gap: the framework's flagship reads/writes don't model the verb idiom we tell users to adopt.
Design / approach
Convert genuine reads to
export const method = 'GET'with a sensiblecachewindow andtags, and annotate mutations withinvalidatesfor the matching tags, so the browser-cache coordinator revalidates after a write. Follow the reference idioms already inexamples/blog/modules/verbdemo/(get-greeting.server.tsfor a cached/tagged GET,bump-greeting.server.tsfor an invalidating mutation).SAFETY:
cacheisprivateby default; only set{ public: true }(or a page-style shared cache) on data identical for every visitor (public blog/changelog reads qualify; per-user/session reads likecurrent-userdo NOT, keep them private and short or uncached). A GET action must be idempotent.Scope: convert where it is genuinely correct, do not force verbs onto write-shaped or per-user actions. Keep
one function per file(a configured file with >1 callable fn is awebjs checkerror).Implementation notes (for the implementing agent)
examples/blog/modules/verbdemo/queries/get-greeting.server.ts(GET +cache+tags),examples/blog/modules/verbdemo/actions/bump-greeting.server.ts(mutation +invalidates). Read these first.website/modules/blog/queries/{get-post,list-posts}.server.ts,website/modules/changelog/queries/list-entries.server.ts-> GET +cache+tags(public, file-backed reads, safe to share).examples/blog/modules/posts/queries/*,modules/comments/queries/list-comments.server.ts-> GET +cache+tags;modules/posts/actions/{create-post,delete-post}.server.ts,modules/comments/actions/create-comment.server.ts-> addinvalidatesfor the post/comment tags.modules/auth/queries/current-user.server.tsstays private/POST (per-session).docs,packages/ui/packages/website: confirm no'use server'actions exist; nothing to convert.packages/cli/lib/create.js:modules/users/queries/list-users.server.ts(~L777) -> addexport const method = 'GET'; export const cache = 30; export const tags = () => ['users'];.modules/users/actions/create-user.server.ts(~L787) -> addexport const invalidates = () => ['users'];. Applies to full-stack AND api templates (both write this users module).packages/cli/lib/saas-template.js:modules/auth/queries/current-user.server.ts(~L146) is per-session, leave POST-default (good teaching counter-example); consider a brief comment noting why it is NOT a public GET.public: trueon a per-user read is a data-leak; reviewers will reject it. Default private.usersquery returns hard-coded sample data with awebjs-scaffold-placeholdermarker; keep the marker/TODO, only add the verb config exports.runtime-rewrite.jstransforms; editing the canonical strings in create.js/saas-template.js covers both runtimes (no parallel bun template)..server.*).publiccache safety mirrors a pageexport const revalidate.test/scaffolds/scaffold-integration.test.js(assert the generated users query declaresmethod = 'GET'and the action declaresinvalidates); counterfactual: the assertion fails before the create.js edit.test/examples/*), assert a converted GET responds withCache-Control/ETag.templates/CONVENTIONS.md/AGENTS.mdif they describe the example modules; run the webjs-doc-sync skill to catch every surface. Runwebjs checkand fix violations.Acceptance criteria
invalidates(verbdemo already covers the standalone demo)invalidates; new apps model the idiom out of the boxwebjs checkpasses; one-function-per-file respected; nopublic: trueon per-user reads