Frictions evaluation · 2026-05-02

Frictions, mapped to library-mode vs framework-mode viability

Each item in frictions.md evaluated against Gastro's stated design goals — fun Go web dev, idiomatic stdlib usage, end-to-end type safety, opinionated structure — and split between the two de-facto consumption modes that the friction document itself reveals.

Source: frictions.md (baseline gastro v0.1.15)  ·  Reference: docs/design.md, README.md, DECISIONS.md  ·  Scope: Sections A–D (E/F deferred per scope agreement)

The two modes, named

Section C of the friction backlog is explicit about a split that the rest of the document leaves implicit: a downstream project (git-pm) decided to skip page routes entirely (01KQ45N0) and call gastro components directly from its own Go handlers, because mutating handlers don't fit a page-rendering model. Naming the modes makes the trade-off legible.

Framework mode primary

Pages live in pages/. Routes are auto-generated. Deps come from WithDeps[T] + From[T](ctx). The host typically does http.ListenAndServe(":4242", router.Handler()) and stops there.

  • Astro/PHP-style DX is the headline pitch.
  • Best fit: read-heavy pages, content sites, classic CRUD.
  • Stresses out under: SSE-morph-heavy apps where 80% of work is mutating handlers.

Library mode emergent

User writes their own router/handlers. Components are invoked through the generated Render.X(props). Auto-generated page routes are ignored or replaced via WithOverride.

  • What git-pm landed on after evaluating both.
  • Best fit: SSE-heavy event-sourced apps, anything where one logical "page" needs many mutating endpoints sharing state.
  • Stresses out under: gastro dev assumptions about main.go layout, hand-coded SSE selectors, manual children plumbing.

Gastro's design goals — fun, idiomatic, type-safe, opinionated — point at framework mode. But the project structure goal also says opinionated, not exclusive: components are exported as a typed Go API (Render.X) on purpose. The two modes are both legitimate; the question is which frictions to close in each.

Verdict matrix

Library-mode item Framework-mode item Both modes

IDFrictionMode affinity PriorityVerdictGoals served
A1CLI not toolchain-pinnableBothP1Ship — directive formIdiomatic Go
A2Generated code committedBothP2After A1; option 1 firstIdiomatic Go
A3Two render paths (Props vs dict)BothP2Hold — v0.1.15 closed the gapType safety
A4Selector ↔ component decouplingLib (today)P2Ship — high leverage, low costType safety
A5Untyped children plumbingBoth, lib stings moreP1Ship — typed Children + slotsType safety
A6No static-asset hashingBothP2Runtime variant firstOpinionated structure
A7Component name collisionBothP2Ship — pure DXFun
B1Snippet / include idiomMostly frameworkP2Defer — tension with opinionFun (vs structure)
B2Per-router dev modeLibP3Ship — trivialIdiomatic Go
C1Page actionsFrameworkP1Spike before committingFun (risks idiom)
C2Route middleware compositionFrameworkP2Ship — additiveIdiomatic Go
C3Programmatic page invocationFrameworkP2Only after C1Type safety
C4Page render error contractFrameworkP2Ship — mostly docsIdiomatic Go
C5Test helpers for page handlersFrameworkP3After C3Fun
D1gastro dev for embedded-package projectsLibP2Ship — gastro watchFun (unlocks lib mode)

Three observations from the matrix that the per-item analysis below expands on:

Group 1 — Mode-neutral plumbing (Section A)

A1

CLI not toolchain-pinnable

Both Ship — directive form

The go run github.com/andrioid/gastro/cmd/gastro@VERSION generate directive matches what protobuf, sqlc, and ent already do — squarely in "idiomatic Go" territory. The importable codegen.Run() alternative locks the codegen entry point as public API and forecloses future refactors; that's a bigger commitment than the friction warrants.

Framework

Solves the "stale CLI builds clean but generates wrong code" surprise during version bumps.

Library

Same benefit, no special handling needed.

Goal alignment: straight idiomatic-Go win. No tension.

A2

Generated code committed

Both After A1; embed-indirection first

Option 1 (point //go:embed at source components/ and pages/ instead of .gastro/templates/ copies) is the safer first move — it shrinks the committed tree without changing the review story. Option 2 (full .gitignore) only becomes safe once A1 makes go generate reproducible; doing it earlier silently amplifies version-skew failures.

Goal alignment: reduces noise, keeps "gastro is a Go project" pitch honest.

A3

Two render paths to maintain

Both Hold — v0.1.15 closed the worst of it

The 2026-04-30 build-time dict validation already converted the painful runtime case ("blank Card field on a typo") into a build error. What remains is mechanical edit-fatigue, not correctness. Both proposed options have real costs:

Recommended: stay with v0.1.15's validation as the long-term answer unless and until template ergonomics show a sharper concrete pain that neither option masks. This is one of the very few items where doing nothing is the design-aligned answer.

A4

Selector ↔ component decoupling

Lib today, both later Ship — high leverage

Library mode hits this acutely: SSE morph handlers in user Go code hardcode "#pm-decisions-list" and break silently when a component's root id= changes. Framework mode hits the same pain the moment C1 (page actions) is in play, because actions emit Datastar patches scoped to selectors.

Library

Drop-in upgrade for any handler doing morph patches today. Zero migration cost — call sites move from string literals to Render.X.Selector incrementally.

Framework

Becomes a typed contract once C1 lands. Codegen can emit selectors as constants on the action's return type.

Goal alignment: end-to-end type safety. The "1 component : N mounts" caveat in the original write-up is real but rare; treating the constant as a default (overridable by literal) is the right pragmatic shape.

A5

Untyped children plumbing

Both, library stings more Ship — typed Children + slots

__children is the last untyped corner of the Props surface. Library mode notices it first because handlers construct Props by hand (e.g. LayoutProps.Detail template.HTML); framework mode only notices when reaching for named slots. A frontmatter slots: field that generates typed template.HTML fields is the cleanest closure of the type-safety gap.

Migration cost is real: existing manual Props that pass children via the dict map break. That should be a tracked deprecation, not a silent flip. The current map-based form can coexist for one or two minor versions with a build warning.

Goal alignment: directly serves "type-safety for components and templates". This is the highest-impact A-item after A4.

A6

No static-asset hashing

Both Runtime variant first

The build-time path-rewriting half is the fragile one (CSS url(), JS-built URLs, attribute parsing edge cases). The runtime hash + redirect half is small, robust, and immediately unlocks long-cache headers without any template rewriting. Ship that behind WithHashedStatic() and revisit build-time rewriting if a real adopter says the runtime form leaves win on the table.

Goal alignment: mild friction with "opinionated structure" (it's an option, not a default), but consistent with "single binary, zero runtime dependencies" — the hash table lives in process memory.

A7

Component name collision warning

Both Ship — pure DX, trivial

A constant-time pre-pass over the component list during compile. The failure mode today (an opaque Go compile error somewhere downstream) burns half an hour the first time and zero minutes after that — but that first half hour is exactly the "Go web dev should be fun" promise breaking. No design tension whatsoever.

Group 2 — Library mode unlocks (B2, D1)

B2

Per-router dev mode

Lib Ship — trivial

A single-process app rarely needs this, but parallel test setups and multi-tenant servers do. The handler-instance refactor (decision 2026-04-26) already moved isDev onto the *Router; this is just exposing a setter that overrides the env var. Trivial in the friction doc is accurate.

D1

gastro dev unusable for embedded-package projects

Lib Ship — split into gastro watch

This is the friction that decides whether library mode is a supported use case or an unofficial accident. Cost is low: the watcher loop in runDev already exists; extracting runWatch is refactoring, not new design. After it lands, gastro dev can deprecate to a thin wrapper around gastro watch + a project-supplied build command, collapsing back to one mental model.

Goal alignment: shipping this signals that "components as a Go API" is a first-class shape, not a workaround. That matters because the typed Render surface is already public; pretending only framework mode exists makes the docs lie about what the binary actually supports.

Group 3 — Framework mode lift (Section C)

C1

Page actions — mutating handlers in .gastro

Framework Spike before committing

This is the single most consequential item in the document. It's the only one that decides whether framework mode is viable for SSE-heavy apps, and it's also the one with the largest design surface: a new keyword, a new return type, a new error contract, a new way for frontmatter to fail. Two countervailing pressures:

Pulls toward shipping

The whole framework-mode pitch (Astro DX) collapses for mutating apps without it. Without C1, the "primary" mode is the one fewer real apps choose.

Pulls toward holding

It introduces a real DSL on top of html/template. The README explicitly promises "no new language to learn". Once shipped, the action surface becomes part of every gastro upgrade story forever.

Recommendation: spike in examples/ before committing to the syntax. Build the git-pm board page two ways — once with library-mode handlers (today's status quo), once with a prototype action declaration in a page. Compare the line counts, the error stories, and the test setup. The friction document already identifies A3 as a prerequisite; A4 and A5 also need to be in place before action returns can be type-checked end-to-end. This is not a one-PR feature.

Goal alignment: serves the "fun" goal best of any item; tensest with the "idiomatic Go" goal of any item. Worth the investment, but only after the supporting type-safety items (A4, A5) are landed so that the new surface ships typed rather than as another map[string]any retreat.

C2

Route middleware composition

Framework Ship — additive

WithMiddleware(pattern, h) as a sibling of the existing WithOverride(pattern, h). Same typo-safety contract (panic on unknown pattern), same option-at-construction shape. Useful even without C1: per-route CSRF/origin checks already need this in any app that mounts both browser pages and SSE endpoints under one router.

One real design question the doc raises: option-per-pattern vs converging on a chi/gin-style middleware chain. Recommendation: keep option-per-pattern. Gastro's whole router is declarative; switching to a fluent chain would be the odd one out.

C3

Programmatic page invocation

Framework Only after C1

Without C1, page frontmatter is just "render this template with these deps", and the generated Render.X(props) already covers the re-render-from-an-SSE-handler case. C3 only earns its keep if pages start holding meaningful logic that morph handlers want to reuse — which is precisely the C1 scenario.

Naming the accessor RenderPage (not folding it into Render) is correct: pages run a different pipeline (deps attachment, request context) than components, and the typed accessor should make that visible.

C4

Page render error contract

Framework Ship — mostly docs

Pure additive. WithErrorHandler(func) + a documented FromOK[T] idiom + an error-handling.md doc. The cost is committing to a public failure contract, not the code. The real win is making framework mode predictable — production adopters won't move workloads to gastro without this.

C5

Test helpers for page handlers

Framework After C3, keep small

A wrapper over httptest that hides the most repetitive plumbing. Worth shipping only if it stays small — six lines per project becomes a maintenance burden the moment it grows a fluent DSL. The friction doc's own framing ("resist the urge to add fluent-DSL features") is correct; the discipline question is whether to commit to that discipline before the package exists.

Group 4 — The standalone tension item (B1)

B1

Snippet / include idiom

Mostly framework Defer — explicit tension with goal #4

The friction doc identifies a real cost (10–20 line shared fragments paying full component overhead), but the solution sits awkwardly against the "opinionated project structure" goal. Adding snippets/ or a snippet: true frontmatter flag means a second authoring concept whose distinction from "component" is "no Props" — a line that's easy to author wrong and creates a refactor with no automated path when a snippet eventually grows parameters.

Two existing escape hatches partly cover this:

Recommendation: hold until a use case appears that neither of those covers cleanly. If A2 lands and the .gastro/ tree stops being committed, much of the "snippet feels like overkill" complaint disappears with it.

Recommended ordering

If items land in dependency order rather than priority order, the path through the document is roughly:

  1. A7, B2, A1 — pure wins, no tension, unblock A2.
  2. D1 — acknowledges library mode is a supported shape; cheap.
  3. A4, A5 — close the type-safety gaps that any C1 design needs to lean on.
  4. C2, C4 — additive framework-mode improvements that don't depend on C1.
  5. A2 — once A1 makes go generate reproducible.
  6. A6 (runtime variant only) — production adopter unlock.
  7. C1 spike — prototype in examples/, measure against library-mode equivalent, then decide.
  8. C3, C5 — only after C1 lands and proves out.
  9. A3, B1 — hold; both have stated goal-tensions and partial existing answers.

The honest meta-question

The C-section exists because a real downstream project ran a bake-off and framework mode lost. Gastro's design goals say framework mode should win that bake-off. There are two coherent responses:

Option A — Make framework mode win

Ship C1 (with C2/C3/C4/C5 in tow), accept that frontmatter becomes a small DSL, accept the "no new language" claim becomes "minimal new language". This is the path that honours the Astro-DX half of the pitch.

Option B — Promote library mode to first-class

Ship D1, A4, A5, B2 cleanly. Document "use Gastro components as a typed Go API; bring your own router for mutating handlers" as a recommended shape. Pages become an opinionated convenience for read-heavy apps. This honours the idiomatic Go half.

The friction document, by listing C1 as P1, leans toward Option A. The effort assessment ("Large", "design work required", "not a quick fix") is the friction document quietly admitting Option A is the harder path. Either is internally consistent; the one mistake would be to spend the C1 effort halfway and ship a feature that doesn't make framework mode decisively better than library mode for the apps currently choosing library mode.