Skip to content

dogfood: close first-pass friction found building a full-stack app #817

Description

@vivek7405

Problem

Building a full-stack todo app on the current scaffold (dogfood) was mostly first-pass correct (webjs check passed on the first run; SSR, the no-JS write path, hydration, optimistic UI, and DB persistence all worked first try), but three avoidable friction points forced correction loops or offered no reference for the agent to copy. All three hit the full-stack default template specifically, the one most agents use for a product with UI. Fixing them moves the default template closer to the "correct on the first pass, no iteration" north-star.

Design / approach

Three independent, small enhancements (implement together or split):

1. Seed the full-stack default with a real example module

The full-stack default creates an EMPTY modules/ dir, so an agent has the modules-architecture CONVENTION (AGENTS.md / CONVENTIONS.md) but no concrete in-scaffold example to mimic. Ironically both other templates DO ship a demo module (verified by scaffolding each): api ships modules/users/ (types.ts + queries/list-users.server.ts + actions/create-user.server.ts) and saas ships modules/auth/ (types.ts + queries/current-user.server.ts + actions/signup.server.ts), i.e. the templates that arguably need the crutch less. Only the full-stack default has an empty modules/. Seed the full-stack default with one tiny, real feature module (a queries/*.server.ts read, an actions/*.server.ts mutation, a types.ts), wired into the example page the same way api wires modules/users into its route, and carry a webjs-scaffold-placeholder marker on it so it follows the existing acknowledge-and-remove pattern (the agent replaces it with the real feature, deletes the marker, and webjs check goes green).

2. Model the typed Metadata import in the example page

The example page.ts uses an UNTYPED export const metadata = { title: ... }. An agent that (correctly) wants to type it has no in-file example and must know Metadata is exported from @webjsdev/core (not @webjsdev/server, where ActionResult lives). I guessed @webjsdev/server and ate a TS2305 from webjs typecheck. Make the scaffold page.ts model import type { Metadata } from '@webjsdev/core' + export const metadata: Metadata = { ... }, so the correct import is copy-pasteable.

3. Document the working Drizzle read pattern (projection typing)

Idiomatic db.select({ col: table.col }).from(t) failed webjs typecheck with TS2554: Expected 0 arguments, but got 1 under the scaffold's drizzle({ client, relations }) (drizzle-orm rc.3 RQBv2 typing). The full-row db.select().from(t) (or db.query.<t>.findFirst({ columns })) types fine. Either document the working read patterns in CONVENTIONS.md / a db recipe so an agent does not hit the projection-overload trip, or investigate whether the projection overload should type-check under RQBv2 and pin/note it. Documentation is the low-risk first step.

4. Encode the corrected patterns in the MCP guided prompts

The @webjsdev/mcp server is how MCP-driven agents learn webjs, and it ships hand-authored guided-workflow PROMPTS (PROMPT_BODIES in packages/mcp/src/mcp-docs.js). The MCP knowledge RESOURCES are the agent-docs/*.md corpus bundled at prepack, so items 2 and 3's doc edits flow to the resources automatically. But the PROMPT bodies are hand-authored and do NOT auto-update, so they must be edited to carry the same correct-first-pass patterns:

  • add_page: currently says "name metadata via a metadata / generateMetadata named export" but never shows the TYPED import. Model import type { Metadata } from '@webjsdev/core' + export const metadata: Metadata = {...} (the exact thing an agent gets wrong, see item 2).
  • add_server_action (and/or a read/query prompt): show the working Drizzle read shape (full-row db.select().from(t) or db.query.<t>.findFirst) so the projection-overload trip (item 3) is never modelled.
  • add_module: confirm its body matches the newly-seeded full-stack example module (item 1) so the MCP and the scaffold agree on the modules shape.

5. Demonstrate optimistic UI (webjs philosophy is optimistic-by-default)

webjs's stated philosophy is to use optimistic UI wherever feasible (root AGENTS.md "Mutations: default to optimistic UI"), but NO scaffold example demonstrates it and the SCAFFOLD's own shipped prose never mentions it. Two gaps to close:

  • Example: the seeded full-stack example module's mutation (item 1) must be called from an interactive component using the DECLARATIVE optimistic(host, { source, update }) API with .add(payload, promise), so the agent has a copy-able optimistic-UI pattern (an instant create/toggle that rolls back on failure), NOT a manual try-catch / cache-and-restore. Model the SAME pattern the taskflow dogfood used (a Todo[] prop + an optimistic store + a mutation action). Keep it PE-safe (a <form> + page action fallback), so it teaches optimistic UI AND progressive enhancement together.
  • Prose: the scaffold's shipped agent docs do NOT prescribe optimistic UI (verified: optimistic appears in root AGENTS.md + agent-docs, but in none of packages/cli/templates/AGENTS.md / CONVENTIONS.md / .cursorrules / .github/copilot-instructions.md / .agents/rules/workflow.md). Add a short "default to optimistic UI for feasible mutations, via optimistic(); skip it for unpredictable / side-effectful / OAuth mutations" guideline to the scaffold prose in lockstep, mirroring the root guidance.

6. Model accessible control labeling (associate clickable text with its control)

A basic a11y/UX practice that an agent (me, dogfooding) MISSED: interactive controls and their clickable text must be ASSOCIATED, so clicking the text/label activates the control (and screen readers announce it). In the taskflow dogfood the toggle was a <button> and the task title a bare <span>, so clicking the text did nothing. The fix is a standard <label for="<control-id>"> (a <button> is a labelable element, so a label click forwards a click to it, which works on BOTH the JS optimistic path AND the no-JS form-submit path). The scaffold's interactive example must model this (<label for> on clickable text, aria-label/aria-pressed on icon-only controls), and the scaffold prose should carry a one-line a11y guideline: "give every interactive control an accessible label, and make clickable text a <label for> (or the control itself) so text clicks activate the control." This is exactly the kind of default an agent should not have to rediscover.

Implementation notes (for the implementing agent)

  • The example page.ts / layout.ts are generated INLINE in packages/cli/lib/create.js (page ~L1134, layout ~L931, export const metadata = { ~L1149), NOT from a templates/app/ file (there is none). Edit the inline template strings there for item 2.
  • modules/ is created empty via the base dir list in create.js (~L288). The api demo module is generated in the if (isApi) { ... } block (~L787-858: modules/users/{queries/list-users.server.ts, actions/create-user.server.ts, types.ts} + a route wrapper). Mirror that shape for full-stack inside the if (!isApi) block for item 1.
  • Landmine (item 1): extending the webjs-scaffold-placeholder marker to a module file means the no-scaffold-placeholder check must scan modules/** too (today it targets app/page.ts + app/layout.ts); confirm the check covers the new file, or wire the demo module into the example page so it rides the existing page/layout placeholder umbrella. A demo module not imported anywhere is servable-gated (fine) but gives the agent nothing to see rendered, so prefer wiring it into the example page like api does with its route.
  • Landmine (item 2): Metadata is a TYPE-ONLY export; keep it import type so the stripper erases it (invariant 10). Do NOT import it from @webjsdev/server.
  • Invariants: the added example must still leave a fresh scaffold failing webjs check ONLY on the intended no-scaffold-placeholder marker(s), nothing else. Prose edits respect invariant 11.
  • Docs surfaces to sync: packages/cli/templates/CONVENTIONS.md (modules section + the db read pattern), agent-docs/metadata.md (the typed-import line) and agent-docs/recipes.md (a page-with-metadata recipe), the docs site if it restates these. Run test/scaffolds/* + packages/cli/test/* after changing the generator.
  • MCP (item 4): edit PROMPT_BODIES in packages/mcp/src/mcp-docs.js (the add_page / add_server_action / add_module entries); the RESOURCES half (agent-docs corpus) auto-syncs via the prepack copy, so only the prompt bodies need hand-editing. Add/adjust a test in packages/mcp/test/*.test.mjs asserting the add_page prompt models the typed Metadata import. No init-primer heading rename, so the primer needs no change.

Acceptance criteria

  • A freshly scaffolded full-stack app ships one real example module (query + action + types.ts) wired into the example page, for the agent to mimic.
  • The seeded mutation is driven from an interactive component using the declarative optimistic() API (instant update + auto-rollback), PE-safe, so the scaffold demonstrates optimistic UI by example; and the scaffold's shipped prose (AGENTS.md / CONVENTIONS.md / the three per-agent rule files) prescribes optimistic-UI-by-default in lockstep.
  • The interactive example models accessible control labeling (<label for> on clickable text so text clicks activate the control, aria-label/aria-pressed on icon-only controls), and the scaffold prose carries a one-line a11y label-association guideline.
  • The example page.ts models import type { Metadata } from '@webjsdev/core' + export const metadata: Metadata = {...}.
  • The working Drizzle read pattern is documented so .select({projection}) does not silently trip an agent (or the typing is verified to accept the projection).
  • A fresh full-stack scaffold fails webjs check ONLY on the intended placeholder marker(s) and passes webjs typecheck.
  • The MCP add_page guided prompt models the typed import type { Metadata } from '@webjsdev/core', and add_server_action shows the working Drizzle read shape (with a packages/mcp/test assertion).
  • Scaffold + template tests updated and green; a counterfactual proves any new no-scaffold-placeholder coverage fires.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions