diff --git a/.agents/skills/webjs/references/runtime.md b/.agents/skills/webjs/references/runtime.md
index 5230b5b16..e8911d707 100644
--- a/.agents/skills/webjs/references/runtime.md
+++ b/.agents/skills/webjs/references/runtime.md
@@ -46,7 +46,7 @@ The 103 Early Hints gap costs only a small first-load latency edge where an edge
webjs create my-app --runtime bun
```
-`--runtime` is orthogonal to `--template`, so it re-flavors any of full-stack, saas, or api. A Bun scaffold emits a `bun.lock`, a pure `oven/bun:1` Dockerfile plus a bun-install CI, and bun-command agent docs. The test, db, and check tooling still runs on Node.
+`--runtime` is orthogonal to `--template`, so it re-flavors either full-stack or api. A Bun scaffold emits a `bun.lock`, a pure `oven/bun:1` Dockerfile plus a bun-install CI, and bun-command agent docs. The test, db, and check tooling still runs on Node.
## Running on Bun
diff --git a/.agents/skills/webjs/references/service-worker.md b/.agents/skills/webjs/references/service-worker.md
index 36970c82c..15c7d52b7 100644
--- a/.agents/skills/webjs/references/service-worker.md
+++ b/.agents/skills/webjs/references/service-worker.md
@@ -11,7 +11,7 @@ Read this when you want an offline experience or an asset cache in a WebJs app,
## What ships and why it is safe
-WebJs's UI scaffolds (full-stack and saas, not the api template) ship a hand-authored service worker at `public/sw.js` and an offline fallback at `public/offline.html`. Both ship **dormant**: they do nothing until the app registers the worker, and the worker only ever registers from JavaScript. So with JS off no worker exists, and pages, links, and forms behave exactly as before. It is opt-in and adds an offline experience plus an asset cache without changing the no-JS baseline.
+WebJs's UI scaffold (full-stack, not the api template) ships a hand-authored service worker at `public/sw.js` and an offline fallback at `public/offline.html`. Both ship **dormant**: they do nothing until the app registers the worker, and the worker only ever registers from JavaScript. So with JS off no worker exists, and pages, links, and forms behave exactly as before. It is opt-in and adds an offline experience plus an asset cache without changing the no-JS baseline.
This is a thin, hand-readable worker built directly on the native Service Worker and Cache Storage APIs. There is no Workbox, no precache framework, and no bundler step, matching WebJs's no-build, close-to-web-standards posture. The file is yours to edit, not a framework internal.
diff --git a/.claude/skills/webjs-scaffold-sync/SKILL.md b/.claude/skills/webjs-scaffold-sync/SKILL.md
index 5a3af38f6..1d6e88ffa 100644
--- a/.claude/skills/webjs-scaffold-sync/SKILL.md
+++ b/.claude/skills/webjs-scaffold-sync/SKILL.md
@@ -75,10 +75,10 @@ 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).
+ gates like `isApi` / `isFullStack` / `!isApi`).
+ - `packages/cli/lib/api-gallery.js` (the api backend-features showcase). Auth
+ is a full-stack GALLERY card now (`templates/gallery/{app/features/auth,
+ modules/auth}`), pruned by `gallery:clear`, not a separate template.
- 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,
@@ -144,7 +144,7 @@ it applies, then update or consciously skip 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 }))"
+ node -e "import('packages/cli/lib/create.js').then(m => m.scaffoldApp('probe', '/tmp/x', { template: 'full-stack', 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).
```
diff --git a/.claude/skills/webjs-start-work/SKILL.md b/.claude/skills/webjs-start-work/SKILL.md
index 4fdbce352..ab42f15ce 100644
--- a/.claude/skills/webjs-start-work/SKILL.md
+++ b/.claude/skills/webjs-start-work/SKILL.md
@@ -144,7 +144,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/` and the generators `packages/cli/lib/{create,saas-template}.js`. Update if the change affects what `webjs create` generates. The scaffold ships a gallery index home + layout + db wiring, a densely-commented feature gallery (`packages/cli/templates/gallery/**`, demos under `app/features/` plus `app/examples/todo`) and the api showcase (`api-gallery.js`), plus one cross-agent skill at `.agents/skills/webjs/` (SKILL.md + references) that the agent grows in place; there are no per-agent rule files. A feature change that agents should know about lands in the skill; a generated-code change lands in the generators, verified with `generate + boot + webjs check`.
+4. **Scaffold templates** under `packages/cli/templates/` and the generators `packages/cli/lib/{create,api-gallery}.js`. Update if the change affects what `webjs create` generates. The scaffold ships a gallery index home + layout + db wiring, a densely-commented feature gallery (`packages/cli/templates/gallery/**`, demos under `app/features/` plus `app/examples/todo`) and the api showcase (`api-gallery.js`), plus one cross-agent skill at `.agents/skills/webjs/` (SKILL.md + references) that the agent grows in place; there are no per-agent rule files. A feature change that agents should know about lands in the skill; a generated-code change lands in the generators, verified with `generate + boot + webjs check`.
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 skill at `.agents/skills/webjs/` (SKILL.md + references/) + `AGENTS.md`, so a docs change is picked up automatically (it is bundled at `prepack`). But if you add or rename a skill reference 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 d14787395..1b6671c47 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -73,7 +73,7 @@ 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 `references/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` + the skill at `.agents/skills/webjs/` (SKILL.md + references/) 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. **Scaffold + skill sync (when a feature changes what apps should do).** The scaffold `webjs create` emits is a gallery index home + a root layout + db wiring, a densely-commented feature gallery (`packages/cli/templates/gallery/**`, single-concept demos under `app/features/` plus the `app/examples/todo` app, shipped in every UI template) and the api backend-features showcase (`packages/cli/lib/api-gallery.js`), plus the one cross-agent skill at `packages/cli/templates/.agents/skills/webjs/` (SKILL.md + references). So when a WebJs feature is added or changed, ask: does the generator (`packages/cli/lib/{create,saas-template,api-gallery}.js`), a gallery demo (`packages/cli/templates/gallery/`), or the agent skill (`.agents/skills/webjs/SKILL.md` + its `references/`) need to move so a freshly scaffolded app and the skill teach the new reality? Verify by generating an app and running `generate + boot + webjs check` (the generators emit strings, so an escaping bug only shows in a freshly generated app). See `framework-dev.md`.
+3. **Scaffold + skill sync (when a feature changes what apps should do).** The scaffold `webjs create` emits is a gallery index home + a root layout + db wiring, a densely-commented feature gallery (`packages/cli/templates/gallery/**`, single-concept demos under `app/features/` plus the `app/examples/todo` app, shipped in every UI template) and the api backend-features showcase (`packages/cli/lib/api-gallery.js`), plus the one cross-agent skill at `packages/cli/templates/.agents/skills/webjs/` (SKILL.md + references). So when a WebJs feature is added or changed, ask: does the generator (`packages/cli/lib/{create,api-gallery}.js`), a gallery demo (`packages/cli/templates/gallery/`), or the agent skill (`.agents/skills/webjs/SKILL.md` + its `references/`) need to move so a freshly scaffolded app and the skill teach the new reality? Verify by generating an app and running `generate + boot + webjs check` (the generators emit strings, so an escaping bug only shows in a freshly generated app). See `framework-dev.md`.
4. **Convention validation.** Run `webjs check` and fix violations.
### Git workflow (mandatory)
@@ -449,7 +449,7 @@ const result = await optimistic(liked, true, () => likePost(postId));
## Scaffolding
-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.
+Two scaffolds exist (do not invent template names): `webjs create ` (full-stack: layout, page, components, modules, Drizzle+SQLite, plus a browsable feature gallery), and `webjs create --template api` (backend-only routes + modules + Drizzle, no SSR). Auth is one of the full-stack gallery cards (login + a signed session + a real protected route), so a full-stack app already carries a promotable auth baseline (this replaced the former `--template saas`). 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 either 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 (full-stack) for any product with UI (todo, blog, dashboard, marketplace, social, e-commerce, a SaaS with accounts/login), `api` for an HTTP/JSON API with no UI; 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. **The scaffold ships a gallery index home, a neutral-palette root layout, db wiring, and a densely-commented feature gallery** (single-concept demos under `app/features/` plus the `app/examples/todo` app, with logic in `modules/`), alongside the one cross-agent skill at `.agents/skills/webjs/` (a routing `SKILL.md` + on-demand `references/`) that teaches how to build. Treat the gallery as browsable reference: read the demos to learn the idioms, then build the app the user asked for by growing the scaffold (add routes under `app/`, components under `components/`, features under `modules//`, keep server-only code behind `.server.ts`) and prune the demos the app does not use (delete the `app/features/` route AND its `modules/`). Give a UI app its own palette by setting the token values in `app/layout.ts`. There is no design gate and no placeholder gate: taste is the agent's job, not the checker's. Docs at https://docs.webjs.dev.
@@ -469,7 +469,7 @@ webjs types # generate .webjs/routes.d.ts (typed Route un
webjs version # print the installed @webjsdev/cli version (also: webjs --version / -v, #975)
webjs help [command] # full usage banner, or per-command usage + Options + Examples (e.g. webjs help routes, #975). Flag forms: webjs --help / -h (banner), webjs --help / -h (that command). typecheck/db/ui --help forward to their wrapped tool; an unknown topic exits 1
webjs typecheck [tsc args...] # the project's own tsc --noEmit
-webjs create [--template api|saas]
+webjs create [--template api]
webjs db # wraps drizzle-kit (+ runs db/seed.server.ts)
webjs ui init | add | list | view
webjs vendor pin|unpin|list|audit|outdated|update [--from PROVIDER] # importmap pinning, .webjs/vendor/importmap.json
diff --git a/README.md b/README.md
index e90d1929a..8d0489c31 100644
--- a/README.md
+++ b/README.md
@@ -32,18 +32,19 @@ TypeScript with zero build step, real SSR with Declarative Shadow DOM.
## Quickstart
-> **AI agents, read this before scaffolding.** Only three templates exist
-> (`full-stack` default, `--template api`, `--template saas`), and the CLI
-> rejects anything else. Always start from one of them. Never hand-roll
-> the directory structure. Default to **full-stack with Drizzle + SQLite**.
-> Never store app data in JSON files, in-memory arrays, or localStorage
-> as a substitute for the database. The scaffold is **reference only**.
+> **AI agents, read this before scaffolding.** Only two templates exist
+> (`full-stack` default, `--template api`), and the CLI rejects anything
+> else. Always start from one of them. Never hand-roll the directory
+> structure. Default to **full-stack with Drizzle + SQLite**. Never store
+> app data in JSON files, in-memory arrays, or localStorage as a
+> substitute for the database. The scaffold is **reference only**.
> Replace the example layout, page, components, and `User` model with
> the app the user actually asked for. Picker:
>
-> - Any product UI (todo, blog, notes, dashboard, marketplace, social, e-commerce…) → **default** (`webjs create `)
+> - Any product UI (todo, blog, notes, dashboard, marketplace, social, e-commerce, a SaaS with accounts…) → **default** (`webjs create `)
> - Backend-only HTTP/JSON API, no UI → **`--template api`**
-> - Auth, login/signup, protected dashboard, SaaS → **`--template saas`**
+>
+> Auth is one of the full-stack gallery cards (login, a signed session, and a real protected route), so a default app already carries a promotable auth baseline.
>
> **The scaffold is a starting point, grown in place.** It ships a gallery index home, a neutral-palette root layout, database wiring, and a densely-commented feature gallery (single-concept demos under `app/features/` plus the `app/examples/todo` app, with logic in `modules/`). The framework context ships as one cross-agent skill, `.agents/skills/webjs/SKILL.md` (a routing skill), alongside `AGENTS.md` and `.agents/rules/workflow.md`. Read the skill and browse the gallery, then build the app the user asked for on top of the scaffold, pruning the demos it does not use.
>
@@ -56,11 +57,11 @@ npm create webjs@latest my-app # full-stack (pages + API + components + Drizzl
cd my-app && npm run dev
# → http://localhost:8080
-# Backend-only API
+# Backend-only API (routes + modules + Drizzle, no UI)
npm create webjs@latest my-api -- --template api
-# SaaS starter (auth + dashboard + Drizzle)
-npm create webjs@latest my-saas -- --template saas
+# Auth (login/signup, session, a protected route) ships as a gallery card
+# in the default full-stack app. No separate template needed.
# Prefer Bun? webjs runs on Node 24+ or Bun. Add --runtime bun to any
# template (it is orthogonal to --template), or scaffold through Bun and
diff --git a/docs/app/docs/ai-first/page.ts b/docs/app/docs/ai-first/page.ts
index 1af5aaae6..bbe086ce0 100644
--- a/docs/app/docs/ai-first/page.ts
+++ b/docs/app/docs/ai-first/page.ts
@@ -124,7 +124,7 @@ export async function createPost(
12. Scaffold + Persistence Defaults
When a layman user says "create a todo app with webjs", the agent should produce a real full-stack app with a real database, not a JSON-file simulation. WebJs enforces this with three guardrails:
- - Exactly three scaffolds.
webjs create <name> (full-stack default), --template api, --template saas. The CLI rejects any other --template value, so an agent can't hallucinate --template todo or --template blog.
+ - Exactly two scaffolds.
webjs create <name> (full-stack default) and --template api. The CLI rejects any other --template value, so an agent can't hallucinate --template todo or --template blog. Auth is one of the full-stack gallery cards, not a separate template.
- Drizzle + SQLite wired up by default. Every scaffold ships
db/schema.server.ts, db/columns.server.ts, db/connection.server.ts (exports db), the webjs.dev.before + webjs.start.before steps running webjs db migrate (idempotent, so a db:generate'd migration applies on the next boot), and npm run db:generate / db:migrate / db:studio / db:seed. The agent edits the schema, runs db:generate, and npm run dev applies it, and won't accidentally fall back to JSON files for persistence.
- Persist with Drizzle, not JSON files. A
data/todos.json or db.json used as a database resets on reload and cannot scale. This is a project convention in AGENTS.md and the shipped skill, so an agent reading it takes the database path.
@@ -132,8 +132,8 @@ export async function createPost(
User asks for… Scaffold
─────────────────────────────────────────────────────────────────────
Todo app, blog, notes, dashboard, marketplace, social default
-HTTP/JSON API only, no UI --template api
-Auth / login / signup / SaaS --template saas
+App with auth / login / signup / accounts (SaaS) default (auth card)
+HTTP/JSON API only, no UI --template api
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.dev. Every API, every recipe, every example lives there. Reach for it before guessing or hand-rolling.
diff --git a/docs/app/docs/auth/page.ts b/docs/app/docs/auth/page.ts
index 40cfb27ca..4042fb6e3 100644
--- a/docs/app/docs/auth/page.ts
+++ b/docs/app/docs/auth/page.ts
@@ -63,7 +63,7 @@ export async function loginWithGoogle() {
export async function logout() {
return signOut({ redirectTo: '/' });
}
- signOut is also reachable as a route: the mounted handlers serve POST /api/auth/signout, so a plain <form method="POST" action="/api/auth/signout"> logs a user out with no JavaScript. That is how the generated saas scaffold renders its logout button.
+ signOut is also reachable as a route: the mounted handlers serve POST /api/auth/signout, so a plain <form method="POST" action="/api/auth/signout"> logs a user out with no JavaScript. That is how the scaffold's auth gallery card renders its logout button.
Showing a failed sign-in
A failed credentials sign-in redirects to ${pages.error}?error=CredentialsSignin, falling back to the home page when pages.error is unset (which silently swallows the failure). Point the error page at your login route, then read the code and render a message:
@@ -80,7 +80,7 @@ export default function LoginPage({ searchParams }) {
<form method="POST" action="/api/auth/signin/credentials">...</form>
\`;
}
- The generated saas scaffold wires exactly this, so a wrong password shows a message on the login page instead of bouncing to the landing page with no feedback.
+ The scaffold's auth gallery card wires exactly this, so a wrong password shows a message on the login page instead of bouncing to the landing page with no feedback.
Callbacks
Customize the session and JWT with callbacks:
diff --git a/docs/app/docs/cache/page.ts b/docs/app/docs/cache/page.ts
index 1066ef839..f3bcc5297 100644
--- a/docs/app/docs/cache/page.ts
+++ b/docs/app/docs/cache/page.ts
@@ -107,7 +107,7 @@ export default async function Blog() {
Safety. Caching is opt-in and conservative, because a wrongly-cached per-user page is a data leak. Declaring revalidate asserts this page is the same for everyone for N seconds. The cache is keyed by the full URL (path plus search) only, with no per-user keying, so a page that reads cookies(), a session, or any per-user data MUST NOT set revalidate. The framework also refuses to cache any response that is not a 200, is a streamed Suspense body, sets any Set-Cookie, or runs under CSP. SSR responses carry no framework cookie (action CSRF is an Origin / Sec-Fetch-Site check, not a token cookie), so a cacheable page is cookieless and safe to share across visitors.
- Framework defense, not just the contract. When the render reads per-user state through a framework helper (cookies(), headers(), getSession(), or auth()), the framework auto-marks the request dynamic and refuses to cache it even if you set revalidate, warning you once with the page path. So a wrong revalidate on a cookie-reading or auth()-gated page fails safe (served fresh) instead of leaking. A saas-dashboard page that does const session = await auth() is auto-excluded. The loud caveat is that this only catches reads THROUGH those helpers. A page that varies its body by an inbound auth cookie or Authorization header but reads it raw (not via cookies() / headers() / getSession() / auth()) and sets no new Set-Cookie WILL be cached and served to a logged-out visitor. Read per-user request state through the framework helpers, which auto-exclude the page, or never set revalidate on a per-user page.
+ Framework defense, not just the contract. When the render reads per-user state through a framework helper (cookies(), headers(), getSession(), or auth()), the framework auto-marks the request dynamic and refuses to cache it even if you set revalidate, warning you once with the page path. So a wrong revalidate on a cookie-reading or auth()-gated page fails safe (served fresh) instead of leaking. An auth-gated dashboard page that does const session = await auth() is auto-excluded. The loud caveat is that this only catches reads THROUGH those helpers. A page that varies its body by an inbound auth cookie or Authorization header but reads it raw (not via cookies() / headers() / getSession() / auth()) and sets no new Set-Cookie WILL be cached and served to a logged-out visitor. Read per-user request state through the framework helpers, which auto-exclude the page, or never set revalidate on a per-user page.
Evict on a write with revalidatePath from a server action:
diff --git a/docs/app/docs/getting-started/page.ts b/docs/app/docs/getting-started/page.ts
index 6c4a3de36..5638affcf 100644
--- a/docs/app/docs/getting-started/page.ts
+++ b/docs/app/docs/getting-started/page.ts
@@ -26,25 +26,22 @@ cd my-app && npm run dev
Create a New App
Using the scaffold
- # full-stack app (pages + API + components + Drizzle/SQLite)
+ # full-stack app (pages + API + components + Drizzle/SQLite + gallery)
webjs create my-app
# backend-only API (route handlers + modules, no pages/components/SSR)
-webjs create my-api --template api
+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.dev.
+ For AI agents: only those two 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, a SaaS with accounts…). Pick --template api only if the user explicitly asks for a backend-only API with no UI. 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).
+ Auth ships as a gallery card. The full-stack app includes an auth card (app/features/auth/ + modules/auth/): login + signup pages, a signed session, an auth API route, createAuth() with a Credentials provider, a Drizzle User model with password hashing, and a genuinely protected route (a segment middleware.ts that redirects an anonymous visitor to login). So a fresh full-stack app already carries a real, promotable auth baseline; gallery:clear strips it (and the rest of the gallery) back to the minimal base.
- The scaffold is a starting point, grown in place. Every UI scaffold ships a gallery index home, a neutral-palette root layout, database wiring, and a densely-commented feature gallery: single-concept demos under app/features/ plus the app/examples/todo app, with logic in modules/ (the api template ships the backend-features showcase under app/api/features/ instead). The framework context you need to build ships as one cross-agent skill, .agents/skills/webjs/SKILL.md (a routing skill), alongside AGENTS.md and the workflow rules in .agents/rules/workflow.md. Read the skill and browse the gallery, then build the app the user asked for on top of the scaffold, editing the home page, layout, and schema in place and pruning the demo routes it does not use.
+ The scaffold is a starting point, grown in place. Every UI scaffold ships a gallery index home, a neutral-palette root layout, database wiring, and a densely-commented feature gallery: single-concept demos under app/features/ plus the app/examples/todo app, with logic in modules/ (the api template ships the backend-features showcase under app/api/features/ instead). The framework context you need to build ships as one cross-agent skill, .agents/skills/webjs/SKILL.md (a routing skill), alongside AGENTS.md and the workflow rules in .agents/rules/workflow.md. Read the skill and browse the gallery, then build the app the user asked for on top of the scaffold, editing the home page, layout, and schema in place and pruning the demo routes it does not use. Both templates ship a one-command reset, npm run gallery:clear: on the full-stack app it sheds the whole feature gallery (and resets the home + schema to a minimal base), and on the api app it sheds the backend-features showcase under app/api/features/ (keeping the health + users endpoints).
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:
+ 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 both). 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
bun create webjs my-app
# the explicit pin-latest form (bun create maps to bunx create-webjs)
diff --git a/examples/blog/.agents/rules/workflow.md b/examples/blog/.agents/rules/workflow.md
index ec1b9441f..662b60d49 100644
--- a/examples/blog/.agents/rules/workflow.md
+++ b/examples/blog/.agents/rules/workflow.md
@@ -19,13 +19,12 @@ cover what you need, the full hosted docs are at **https://docs.webjs.dev**.
the example `User` model, the example users module, etc. with the app the
user actually asked for. Do not ship "Hello from " as the
deliverable.
-- **Only three templates exist:** `webjs create ` (default full-stack),
- `--template api`, `--template saas`. The CLI rejects any other `--template`
- value. Pick:
- - Any product UI (todo, blog, dashboard, marketplace, social) goes through
- the default template.
+- **Only two templates exist:** `webjs create ` (default full-stack) and
+ `--template api`. The CLI rejects any other `--template` value. Pick:
+ - Any product UI (todo, blog, dashboard, marketplace, social, a SaaS with
+ accounts) goes through the default template. Auth is one of the full-stack
+ gallery cards (login, session, a protected route), not a separate template.
- HTTP/JSON API only, no UI, uses `--template api`.
- - Auth / login / signup / SaaS uses `--template saas`.
## Before starting ANY work
diff --git a/examples/blog/.github/copilot-instructions.md b/examples/blog/.github/copilot-instructions.md
index d6fc98f7e..ce2988b9f 100644
--- a/examples/blog/.github/copilot-instructions.md
+++ b/examples/blog/.github/copilot-instructions.md
@@ -18,12 +18,12 @@ the full hosted docs are at **https://docs.webjs.dev**.
`app/page.ts`, the example `User` model, the example users module, etc.
with the app the user actually asked for. Don't ship "Hello from
" as the deliverable.
-- **Only three templates exist:** `webjs create ` (default
- full-stack), `--template api`, `--template saas`. The CLI rejects any
- other `--template` value. Pick:
- - Any product UI (todo, blog, dashboard, marketplace, social…) → default
+- **Only two templates exist:** `webjs create ` (default
+ full-stack) and `--template api`. The CLI rejects any other
+ `--template` value. Pick:
+ - Any product UI (todo, blog, dashboard, marketplace, social, a SaaS with accounts…) → default
- HTTP/JSON API only, no UI → `--template api`
- - Auth / login / signup / SaaS → `--template saas`
+ - Auth (login, session, a protected route) ships as a full-stack gallery card, not a template.
## Before starting ANY work
diff --git a/examples/blog/CONVENTIONS.md b/examples/blog/CONVENTIONS.md
index 7538720e1..38372cbc7 100644
--- a/examples/blog/CONVENTIONS.md
+++ b/examples/blog/CONVENTIONS.md
@@ -874,9 +874,8 @@ no per-project disabling. Run `webjs check` to validate, and
Create new projects with `webjs create`:
```sh
-webjs create # full-stack (default)
-webjs create --template api # backend-only API
-webjs create --template saas # auth + dashboard + Drizzle User model
+webjs create # full-stack (default; auth ships as a gallery card)
+webjs create --template api # backend-only API (routes + modules + Drizzle)
```
**Route-wrapping pattern (especially for `--template api` apps):**
diff --git a/packages/cli/AGENTS.md b/packages/cli/AGENTS.md
index 53ca62dda..1e0e44189 100644
--- a/packages/cli/AGENTS.md
+++ b/packages/cli/AGENTS.md
@@ -86,10 +86,11 @@ lib/
lockfile + applies the
runtime-rewrite transforms to the copied deploy +
agent-config files when bun.
- saas-template.js Extra files written when --template saas:
- auth + login/signup + protected dashboard
- + Drizzle User model. `writeSaasFiles(appDir, {runtime})`
- bun-ifies the generated auth-test setup comments.
+ api-gallery.js Backend-features showcase for --template api:
+ JSON/HTTP endpoints under app/api/features/ (the
+ api counterpart of the UI gallery). Auth is a
+ full-stack GALLERY card now (templates/gallery/
+ {app/features/auth,modules/auth}), not a template.
runtime-rewrite.js Pure transforms (#541) that DERIVE the bun-mode
variant of each canonical node template:
`bunifyProse` (npm->bun command forms in markdown),
diff --git a/packages/cli/README.md b/packages/cli/README.md
index ba8ed80a4..b8fcfa1e6 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -38,9 +38,8 @@ Both `webjs create` and `create-webjs-app` auto-install dependencies in the new
## Commands
```sh
-webjs create # scaffold a full-stack app (default)
-webjs create --template api # backend-only API app
-webjs create --template saas # auth + dashboard + Drizzle User model
+webjs create # scaffold a full-stack app (default; auth ships as a gallery card)
+webjs create --template api # backend-only API app (routes + modules + Drizzle)
webjs dev # dev server with live reload (runs webjs.dev.before, e.g. webjs db migrate, then serves; npm run dev is a thin alias)
webjs start # production server (no build step, serves source directly)
diff --git a/packages/cli/bin/webjs.js b/packages/cli/bin/webjs.js
index 81905fd8c..b227aa0db 100755
--- a/packages/cli/bin/webjs.js
+++ b/packages/cli/bin/webjs.js
@@ -43,10 +43,10 @@ if (cmd !== 'help' && cmd !== undefined && !wantsHelp && !wantsVersion) {
}
}
-// Exactly three scaffolds exist. Keep this list as the single source of
+// Exactly two scaffolds exist. Keep this list as the single source of
// truth. AI-agent docs in README.md / AGENTS.md / .cursorrules /
// .agents/rules/workflow.md / .github/copilot-instructions.md mirror it.
-const TEMPLATES = ['full-stack', 'api', 'saas'];
+const TEMPLATES = ['full-stack', 'api'];
const USAGE = `webjs commands:
webjs dev [--port 8080] [--no-hot] Start dev server with live reload
@@ -60,8 +60,8 @@ const USAGE = `webjs commands:
--json emits the structured results (with stable codes). --strict also fails the exit on warnings
webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
webjs typecheck [tsc args...] Type-check the app with the project's tsc --noEmit (non-zero on errors)
- webjs create [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--no-install] Scaffold a new webjs app
- (only 3 templates exist. default: full-stack, Drizzle, --db sqlite, --runtime node)
+ webjs create [--template full-stack|api] [--db sqlite|postgres] [--runtime node|bun] [--no-install] Scaffold a new webjs app
+ (only 2 templates exist. default: full-stack, Drizzle, --db sqlite, --runtime node)
--runtime bun emits a Bun-flavored app (bun.lock, bun Dockerfile/CI, bun docs);
also auto-detected when run via "bun create webjs".
Auto-runs the detected package manager's install in the new dir
@@ -160,10 +160,10 @@ const HELP = {
examples: ['webjs typecheck', 'webjs typecheck --watch'],
},
create: {
- usage: 'webjs create [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--no-install]',
+ usage: 'webjs create [--template full-stack|api] [--db sqlite|postgres] [--runtime node|bun] [--no-install]',
summary: 'Scaffold a new app. Defaults: full-stack template, Drizzle + SQLite, Node runtime.',
options: [
- { flag: '--template ', description: 'full-stack (default), api, or saas.' },
+ { flag: '--template ', description: 'full-stack (default) or api (backend-only, no UI).' },
{ flag: '--db ', description: 'sqlite (default) or postgres.' },
{ flag: '--runtime ', description: 'node (default) or bun.' },
{ flag: '--no-install', description: 'Skip the package-manager install step.' },
@@ -171,7 +171,7 @@ const HELP = {
examples: [
'webjs create my-app',
'webjs create my-api --template api',
- 'webjs create my-saas --template saas --db postgres',
+ 'webjs create my-api --template api --db postgres',
'webjs create my-app --runtime bun',
],
},
@@ -846,7 +846,7 @@ async function main() {
case 'create': {
const name = rest[0];
if (!name || name.startsWith('-')) {
- console.error('Usage: webjs create [--template full-stack|api|saas]');
+ console.error('Usage: webjs create [--template full-stack|api]');
process.exit(1);
}
const template = flag(rest, '--template', 'full-stack');
@@ -856,16 +856,16 @@ async function main() {
// on which scaffold to pick for which kind of app.
console.error(`Error: unknown template '${template}'.
-Only three scaffolds exist:
- full-stack (default): pages + components + API + Drizzle/SQLite.
- Pick this for any app the user describes in product terms
- (todo app, blog, dashboard, marketplace, social feed, …).
- api backend-only: route handlers + modules, no pages/SSR.
- Pick this only if the user explicitly asks for an HTTP/JSON
- API with no UI.
- saas auth + login/signup + protected dashboard + Drizzle User
- model. Pick this only if the user explicitly asks for auth
- or a SaaS-shaped product.
+Only two scaffolds exist:
+ full-stack (default): pages + components + API + Drizzle/SQLite, plus a
+ browsable feature gallery. Auth is one of the gallery cards
+ (login + session + a protected route), so a full-stack app
+ already carries a real auth baseline. Pick this for any app
+ the user describes in product terms (todo, blog, dashboard,
+ marketplace, social feed, a SaaS with accounts, …).
+ api backend-only: route handlers + modules + Drizzle/SQLite, no
+ pages/SSR. Pick this only if the user explicitly asks for an
+ HTTP/JSON API with no UI.
The scaffold is a starting point. Replace the example layout/page/
components/schema with the actual app the user requested. Use Drizzle +
diff --git a/packages/cli/lib/api-gallery.js b/packages/cli/lib/api-gallery.js
index 214753012..3172c7df2 100644
--- a/packages/cli/lib/api-gallery.js
+++ b/packages/cli/lib/api-gallery.js
@@ -2,7 +2,7 @@
* 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
+ * Extracted here 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.
*/
diff --git a/packages/cli/lib/create.js b/packages/cli/lib/create.js
index 336e42bce..4dfb96d93 100644
--- a/packages/cli/lib/create.js
+++ b/packages/cli/lib/create.js
@@ -18,7 +18,6 @@ import { existsSync } from 'node:fs';
import { createRequire } from 'node:module';
import { spawnSync } from 'node:child_process';
import { bunifyProse, bunifyDockerfile, bunifyCompose, bunifyCi } from './runtime-rewrite.js';
-import { leanComponentSource } from './lean-copy.js';
/**
* Detect which package manager invoked us. Reads `npm_config_user_agent`,
@@ -106,60 +105,6 @@ function resolveUiRegistryRoot() {
}
const UI_REGISTRY_ROOT = resolveUiRegistryRoot();
-/**
- * Read a single @webjsdev/ui registry component, rewrite its relative import
- * of `../lib/utils.ts` to the scaffolded app's aliased path so it resolves
- * when written to `components/ui/.ts`. The scaffold puts cn() at
- * `lib/utils/cn.ts` (folder-grouped with other browser-safe helpers), so the
- * alias form is `#lib/utils/cn.ts` (#555/#556).
- *
- * @param {string} name component name without `.ts` (e.g. 'button')
- * @returns {Promise} source with import rewritten
- */
-async function readUiComponent(name) {
- const src = join(UI_REGISTRY_ROOT, 'components', `${name}.ts`);
- const raw = await readFile(src, 'utf8');
- // The registry component imports cn() via a relative `../lib/utils.ts`; rewrite
- // it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
- const rewritten = raw
- .replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
- .replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
- // onBeforeCache lives in its own client-only module so cn() stays pure (#819).
- .replaceAll("'../lib/dom.ts'", "'#lib/utils/dom.ts'")
- .replaceAll('"../lib/dom.ts"', '"#lib/utils/dom.ts"');
- // Strip the worked @example from a Tier-1 helper (same as `webjs ui add`), so
- // the scaffolded component is lean and the example is served on demand. The
- // shared helper is used by the saas-template copier too, so they cannot drift.
- return leanComponentSource(rewritten, name);
-}
-
-/**
- * Copy a list of @webjsdev/ui registry components into the scaffolded app
- * under `components/ui/`. Throws if any name is missing from the registry,
- * since the scaffold's generated pages import these by name and a missing
- * file would produce ERR_MODULE_NOT_FOUND at first request. Caller must
- * have already invoked assertUiRegistryAvailable().
- *
- * @param {string} appDir destination app root
- * @param {string[]} names list of component file basenames (without `.ts`)
- */
-async function copyUiComponents(appDir, names) {
- const uiDir = join(appDir, 'components', 'ui');
- await mkdir(uiDir, { recursive: true });
- for (const n of names) {
- const src = join(UI_REGISTRY_ROOT, 'components', `${n}.ts`);
- if (!existsSync(src)) {
- throw new Error(
- `@webjsdev/ui registry is missing component '${n}.ts' at ${src}. ` +
- `The scaffold's example pages import this component by name. ` +
- `Either the registry was published incompletely or the scaffold's ` +
- `component list is out of sync with the registry.`,
- );
- }
- await writeFile(join(uiDir, `${n}.ts`), await readUiComponent(n));
- }
-}
-
/**
* Copy the example gallery (idiomatic, densely-commented working examples) into
* the scaffolded app. Merges `templates/gallery/{app,modules}` over the app so
@@ -177,7 +122,9 @@ async function copyUiComponents(appDir, names) {
*/
async function copyGallery(appDir) {
const galleryDir = join(TEMPLATES, 'gallery');
- for (const sub of ['app', 'modules']) {
+ // `test` carries the auth card's real request-pipeline test (test/auth); it
+ // ships with the gallery and is pruned by gallery:clear alongside the card.
+ for (const sub of ['app', 'modules', 'test']) {
await cp(join(galleryDir, sub), join(appDir, sub), { recursive: true });
}
}
@@ -312,21 +259,19 @@ export async function scaffoldApp(name, cwd, opts = {}) {
const shouldInstall = opts.install === true;
// Defence in depth. The CLI already validates this, but library
// callers (tests, programmatic use) might pass anything.
- const VALID_TEMPLATES = ['full-stack', 'api', 'saas'];
+ const VALID_TEMPLATES = ['full-stack', 'api'];
if (!VALID_TEMPLATES.includes(template)) {
throw new Error(
`Unknown template '${template}'. Only ${VALID_TEMPLATES.join(' / ')} exist.`,
);
}
const isApi = template === 'api';
- const isSaas = template === 'saas';
- // The example gallery ships in every UI scaffold (full-stack AND saas). The
- // copyGallery gate below is !isApi, since only the api template has no UI. saas
- // overwrites db/schema.server.ts with its own schema (which includes the
- // gallery's todos table) and renders the gallery below its auth landing.
- // isFullStack distinguishes the plain full-stack app from saas for the parts
- // that differ (its own home page and the create.js-written schema).
- const isFullStack = !isApi && !isSaas;
+ // The example gallery ships in the one UI template (not api, which has no UI),
+ // so the copyGallery gate below is !isApi. Auth is one of the gallery cards
+ // (app/features/auth + modules/auth), so a UI app ships a real, prunable auth
+ // baseline. `isFullStack` names the UI template for the parts that differ from
+ // api (the todos table + the passwordHash column the auth card needs).
+ const isFullStack = !isApi;
// Database dialect (#563): sqlite (default) or postgres. Drizzle is the ORM;
// the schema/queries/actions are identical across dialects, only db/columns
@@ -420,8 +365,13 @@ export async function scaffoldApp(name, cwd, opts = {}) {
// app runs the compiler under Bun (its image has no Node), a Node app runs
// it directly.
...(isApi ? {} : { 'css:build': cssBuildCmd }),
- // Shed the demo gallery to a clean, buildable base (scripts/clear-gallery.mjs).
- ...(isApi ? {} : { 'gallery:clear': isBun ? 'bun scripts/clear-gallery.mjs' : 'node scripts/clear-gallery.mjs' }),
+ // Shed the demo gallery / backend-features showcase to a clean, buildable
+ // base. The UI template runs clear-gallery.mjs; the api template runs
+ // clear-api-gallery.mjs (its showcase is app/api/features, not app/features).
+ 'gallery:clear': (() => {
+ const script = isApi ? 'clear-api-gallery.mjs' : 'clear-gallery.mjs';
+ return isBun ? `bun scripts/${script}` : `node scripts/${script}`;
+ })(),
dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
start: isBun ? 'bun --bun webjs start' : 'webjs start',
test: 'webjs test',
@@ -763,7 +713,10 @@ export const users = table('users', {
name: text(),
// JSON column: a structured value persisted as JSON, typed via json().
// Same helper works on SQLite and Postgres. Delete if you do not need it.
- settings: json<{ theme?: string }>(),
+ settings: json<{ theme?: string }>(),${isFullStack ? `
+ // The auth gallery card (app/features/auth) signs credentials against this.
+ // gallery:clear removes this column with the rest of the auth surface.
+ passwordHash: text(),` : ''}
createdAt: createdAt(),
});
${isFullStack ? `
@@ -1040,10 +993,18 @@ export type ActionResult =
// counterpart of the UI gallery. Prune what you skip.
const { writeApiGallery } = await import('./api-gallery.js');
await writeApiGallery(appDir);
+
+ // The showcase-reset script (wired as `gallery:clear` for the api template).
+ // It sheds app/api/features + its modules back to the health + users base.
+ const apiClearSrc = join(TEMPLATES, 'scripts', 'clear-api-gallery.mjs');
+ if (existsSync(apiClearSrc)) {
+ await mkdir(join(appDir, 'scripts'), { recursive: true });
+ await cp(apiClearSrc, join(appDir, 'scripts', 'clear-api-gallery.mjs'));
+ }
}
if (!isApi) {
- // Full-stack and SaaS templates: layout + page + theme toggle + Tailwind
+ // The UI template: layout + page + theme toggle + Tailwind
// The Tailwind stylesheet is compiled from public/input.css (written below)
// to a STATIC public/tailwind.css by css:build, and lib/utils/ui.ts helpers
@@ -1053,8 +1014,8 @@ export type ActionResult =
const publicDir = join(appDir, 'public');
await mkdir(publicDir, { recursive: true });
// Progressive-enhancement service worker (#271): ship the opt-in offline
- // primitive (the worker + its offline fallback) into the UI scaffolds
- // (full-stack / saas; this block is api-excluded since api has no UI).
+ // primitive (the worker + its offline fallback) into the UI scaffold
+ // (this block is api-excluded since api has no UI).
// Dormant until the app registers it (see the skill's references/service-worker.md);
// it never changes the JS-disabled baseline.
for (const swFile of ['sw.js', 'offline.html']) {
@@ -1087,13 +1048,9 @@ export type ActionResult =
// styles/globals.css (the @webjsdev/ui theme).
await writeUiBootstrap(appDir);
- // The saas auth pages import a few ui-* primitives. A full-stack app adds
- // any component on demand with `webjs ui add `.
- if (isSaas) {
- await copyUiComponents(appDir, [
- 'button', 'card', 'alert', 'badge', 'separator', 'label', 'input',
- ]);
- }
+ // The gallery cards style with plain Tailwind (the app is pre-initialised for
+ // `webjs ui add ` via writeUiBootstrap above, but ships no components
+ // until you add them on demand).
// The @webjsdev/ui theme (`--color-primary`, `--color-card`, the @theme maps,
// @custom-variant, @keyframes) plus the app @theme mappings are compiled from
@@ -1136,10 +1093,10 @@ ${uiThemeRaw}
// The gallery: idiomatic, densely-commented single-feature demos under
// app/features/ plus one whole example app under app/examples/, with logic
- // in modules/, all linked from the home page below. Shipped in every UI
- // scaffold (full-stack AND saas) so an agent gains context by browsing real
- // working code; prune per-feature (delete the route + its module) for what
- // the app does not use.
+ // in modules/, all linked from the home page below. Shipped in the UI
+ // scaffold so an agent gains context by browsing real working code; prune
+ // per-feature (delete the route + its module) for what the app does not use,
+ // or shed the whole gallery at once with `gallery:clear`.
await copyGallery(appDir);
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
@@ -1322,11 +1279,7 @@ export default function RootLayout({ children }: { children: unknown }) {
// demo and the example app, and a footer with the docs + source links. Treat it
// as a starting point: prune the demos you do not use (delete the
// app/features/ route AND its modules/), then reshape this page into the
- // app's real landing page. For the saas template a login/signup CTA row is
- // spliced under the tagline.
- const homeAuthLinks = isSaas
- ? '\n '
- : '';
+ // app's real landing page.
await writeFile(join(appDir, 'app', 'page.ts'), `import { html } from '@webjsdev/core';
export const metadata = {
@@ -1340,11 +1293,13 @@ export const metadata = {
const FEATURES = [
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
{ href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
+ { href: '/features/auth', title: 'Auth', blurb: 'Password login on createAuth, a signed session cookie, and a real protected route that redirects anonymous visitors to login.' },
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
{ href: '/features/server-actions', title: 'Server actions', blurb: 'A use-server RPC action next to a server-only .server.ts utility, and why the boundary matters.' },
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
{ href: '/features/async-render', title: 'Async render', blurb: 'A component that awaits server data in async render(), so the resolved value is in the first paint.' },
{ href: '/features/streaming', title: 'Streaming actions', blurb: 'A use-server action that returns an async generator, streamed to the call site token by token with for await.' },
+ { href: '/features/stream', title: 'Stream updates', blurb: 'The element: renderStream() applies surgical append / replace / remove DOM updates by target id, no region redraw.' },
{ href: '/features/suspense', title: 'Suspense boundary', blurb: 'The element: a first-paint fallback for a SLOW component, with the resolved content streamed in.' },
{ href: '/features/view-transitions', title: 'View transitions', blurb: 'The opt-in view-transition meta cross-fades a soft navigation, with a data-webjs-permanent element persisted across the swap.' },
{ href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
@@ -1379,7 +1334,7 @@ export default function Home() {
AI-first and web-components-first. Server-rendered, progressively enhanced, and buildless.
-
${homeAuthLinks}
+
@@ -1508,12 +1463,6 @@ ThemeToggle.register('theme-toggle');
`);
} // end if (!isApi)
- // --- SaaS template extras: auth, dashboard, drizzle User model ---
- if (isSaas) {
- const { writeSaasFiles } = await import('./saas-template.js');
- await writeSaasFiles(appDir, { runtime });
- }
-
// AGENTS.md is already in place via the shared `templateFiles` loop
// earlier in this function, so no framework-root fallback needed.
@@ -1534,20 +1483,11 @@ ThemeToggle.register('theme-toggle');
modules/users/{actions,queries,types.ts} ← routes over server actions
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
${guide}
-`);
- } else if (isSaas) {
- console.log(` ${name}/
- app/{layout,page}.ts, login/, signup/
- app/dashboard/{page,settings,middleware}.ts ← protected
- app/api/auth/[...path]/route.ts ← auth API
- components/ui/*, components/theme-toggle.ts
- modules/auth/*, lib/{auth,password}.server.ts
- db/{schema,columns,connection}.server.ts ← Drizzle (User model)
- ${guide}
`);
} else {
console.log(` ${name}/
- app/{layout,page}.ts ← a minimal home to grow in place
+ app/{layout,page}.ts ← gallery home; gallery:clear grows in place
+ app/features/*, modules/* ← browsable demos (incl. auth: login + protected route)
components/theme-toggle.ts
public/input.css ← Tailwind entry (compiles to public/tailwind.css)
db/{schema,columns,connection}.server.ts ← Drizzle
@@ -1596,9 +1536,9 @@ ThemeToggle.register('theme-toggle');
// Next-steps banner prints LAST so the actionable command is the
// final thing on screen, never buried above the AI-agent guidance.
// Single copy-paste line so the user can move from "scaffold done"
- // to "dev server up" in one command. The full-stack and saas
- // templates ship with @webjsdev/ui already initialised; the api
- // template has no UI but may add one later.
+ // to "dev server up" in one command. The full-stack template ships
+ // with @webjsdev/ui already initialised; the api template has no UI
+ // but may add one later.
const installSegment = installed ? '' : `${pm} install && `;
// The shipped schema is applied on the first `run dev` (webjs.*.before runs
// `db migrate`), but only if a migration FILE exists. When we installed, we
diff --git a/packages/cli/lib/lean-copy.js b/packages/cli/lib/lean-copy.js
deleted file mode 100644
index fe260d8d7..000000000
--- a/packages/cli/lib/lean-copy.js
+++ /dev/null
@@ -1,43 +0,0 @@
-/**
- * The scaffold's lean-copy of a ui component (#983).
- *
- * `webjs create` copies a few `@webjsdev/ui` registry components into a
- * generated app. To match what `webjs ui add` writes, a Tier-1 helper's worked
- * `@example` is stripped (the example is served on demand by `webjs ui view` /
- * the MCP `ui` tool), while a Tier-2 element file is kept whole. Both scaffold
- * copiers (`create.js` and `saas-template.js`) go through THIS one helper so
- * they cannot drift.
- *
- * The strip primitives live in `@webjsdev/ui/registry/extract`; if that subpath
- * cannot be resolved, this degrades to a no-op (keep the example) so the strip
- * is never a reason `webjs create` fails.
- *
- * @module lean-copy
- */
-
-let _mod = null;
-
-async function loadPrimitives() {
- if (_mod) return _mod;
- try {
- const m = await import('@webjsdev/ui/registry/extract');
- _mod = { stripExample: m.stripExample, isCustomElementSource: m.isCustomElementSource };
- } catch {
- _mod = { stripExample: (s) => s, isCustomElementSource: () => true };
- }
- return _mod;
-}
-
-/**
- * Return the component source as `webjs ui add` would write it: a Tier-1 helper
- * has its worked `@example` stripped and a pointer left; a Tier-2 element is
- * returned unchanged.
- *
- * @param {string} source the component source (imports already rewritten)
- * @param {string} name the component name (for the pointer)
- * @returns {Promise}
- */
-export async function leanComponentSource(source, name) {
- const { stripExample, isCustomElementSource } = await loadPrimitives();
- return isCustomElementSource(source) ? source : stripExample(source, name);
-}
diff --git a/packages/cli/lib/saas-template.js b/packages/cli/lib/saas-template.js
deleted file mode 100644
index 441d6115c..000000000
--- a/packages/cli/lib/saas-template.js
+++ /dev/null
@@ -1,568 +0,0 @@
-/**
- * SaaS template files for `webjs create --template saas`.
- * Extracted to avoid nested template literal escaping issues.
- */
-
-import { mkdir, writeFile, readFile } from 'node:fs/promises';
-import { bunifyProse } from './runtime-rewrite.js';
-import { leanComponentSource } from './lean-copy.js';
-import { existsSync } from 'node:fs';
-import { join, resolve, dirname } from 'node:path';
-import { fileURLToPath } from 'node:url';
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const UI_REGISTRY_ROOT = resolve(
- __dirname, '..', '..', 'ui', 'packages', 'registry',
-);
-
-/**
- * Read a registry component and rewrite its `'#lib/utils.ts'` import for
- * the scaffolded app's `components/ui/.ts` layout (the `#lib/utils/cn.ts`
- * alias). Mirrors the helper in `create.js`, kept private here to avoid coupling.
- */
-async function readUiComponent(name) {
- const src = join(UI_REGISTRY_ROOT, 'components', `${name}.ts`);
- if (!existsSync(src)) return null;
- const raw = await readFile(src, 'utf8');
- // The registry component imports cn() via a relative `../lib/utils.ts`; rewrite
- // it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
- const rewritten = raw
- .replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
- .replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
- // onBeforeCache lives in its own client-only module so cn() stays pure (#819).
- // Without this rewrite dialog.ts keeps the registry-relative `../lib/dom.ts`
- // (which resolves to a nonexistent components/lib/dom.ts) and fails typecheck.
- .replaceAll("'../lib/dom.ts'", "'#lib/utils/dom.ts'")
- .replaceAll('"../lib/dom.ts"', '"#lib/utils/dom.ts"');
- // Strip a Tier-1 helper's worked @example (same as create.js + `webjs ui add`)
- // so switch / checkbox are lean, not just the full-stack base set (#983).
- return leanComponentSource(rewritten, name);
-}
-
-/** Copy named registry components into `/components/ui/`. */
-async function copyUiComponents(appDir, names) {
- const uiDir = join(appDir, 'components', 'ui');
- await mkdir(uiDir, { recursive: true });
- for (const n of names) {
- const content = await readUiComponent(n);
- if (content == null) continue;
- await writeFile(join(uiDir, `${n}.ts`), content);
- }
-}
-
-/**
- * @param {string} appDir
- * @param {{ runtime?: 'node'|'bun' }} [opts]
- */
-export async function writeSaasFiles(appDir, opts = {}) {
- const isBun = opts.runtime === 'bun';
- // SaaS pages use auth forms, so copy the extra ui-* components on top of
- // the standard set the full-stack scaffold already wrote. Pre-importing
- // them in login/signup/dashboard pages below means the dev server will
- // SSR these elements with full styling on first paint.
- // `form` and `field` are deferred to v2 (see packages/ui/AGENTS.md) -
- // the saas auth pages use raw