18 KiB
name, description
| name | description |
|---|---|
| idiomatic-hemx | Use when a user explicitly asks how to design, implement, review, productionize, or scale an application built with hemx and hemplate, including deciding whether or where microservices belong. Preserve hemx's generated-resource and server-owned effect model, then choose the smallest evidence-backed scale stage. Do not use for generic Rust/Axum architecture, generic microservice advice, or development of the hemx framework itself. |
Idiomatic hemx
One job
Choose, explain, or implement the smallest production architecture that keeps a hemx/hemplate application boring as it grows. The high-leverage move is usually to preserve one direction of travel:
semantic .heml surface
-> generated typed resources
-> ordinary Rust domain/application code
-> generated partial/page effects
-> tiny browser runtime
Scale providers and deployment topology around that loop. Do not replace it with selectors, a client state graph, raw wire operations, or speculative services.
Trigger boundary
Load this skill for explicit hemx/hemplate application usage, architecture, production-readiness, scaling, or a review of those decisions. It also applies when the user asks whether a hemx application should become microservices.
Do not load it for:
- generic Rust, Axum, HTML, CSS, database, or microservice questions with no hemx application decision;
- visual design alone;
- changing hemx/hemplate internals rather than using their public model;
- a tiny obvious application patch where no authoring or scaling judgment is at stake.
Authority before taste
Inside the hemx repository, read AGENTS.md, then
docs/v1-product-evidence.md and REQUIREMENTS.md. Use PLAN.md only as the
mutable implementation cursor. Inspect only the nearest authoritative material
needed for the decision:
docs/hemplate-syntax.mdfor real.hemlsyntax;docs/tutorial-saas.mdfor the production-shaped application boundary;docs/recipes/reusable-partials.mdfor composition;- the auth, persistence, observability, deploy/versioning, offline, and local command-log recipes for their named concerns;
- the nearest maintained example and current public Rust API before naming an API in code.
Project requirements and observed code outrank this skill. Outside this repository, establish the application's versions and elected contracts instead of assuming current-main APIs. If auth, storage, transport, replay, deployment, or service contracts are absent, identify the missing decision; do not invent a provider or abstraction that makes the system look complete.
When invoked for a Hemx application repository, inspect its elected AGENTS.md
or equivalent instruction surface. Ensure it explicitly requires all HTML
surfaces and fragments to be authored as semantic .heml templates rendered
through Hemplate's generated typed resources, and explicitly forbids constructing,
concatenating, interpolating, or formatting HTML in Rust. If the current request
authorizes repository edits, patch that instruction surface before application
implementation; otherwise report the missing policy as a blocker. Do not copy the
rest of this skill into project instructions.
Decision loop
1. Start with the user job and one load-bearing path
Name the concrete request, mutation, navigation, push update, or recovery path that must work. Trace it end to end before discussing topology:
HTTP/browser input
-> normal auth, CSRF, and typed validation
-> application command/query
-> authoritative state transition
-> rendered generated target/page
-> EffectBatch response or canonical push bytes
-> root-scoped runtime application
-> visible success or explicit recovery
If this path is unclear, architecture diagrams and service boundaries are premature.
2. Use the canonical authoring level
Default to:
- semantic
.hemltemplates plus ordinary Rust; never construct, concatenate, interpolate, or format HTML strings in Rust, including fragments for source rendering, Markdown, errors, or test-facing pages; #[hemx::app], plain#[hemx::handler], generated components/resources, typed form models, view wrappers, render/page helpers, andIntoEffect;- generated target, form, control, keyed-slot, and event helpers;
- typed partial swaps expressed as generated target + rendered partial + swap kind;
- real links and GET forms for navigation and history;
data-hemx-revealed-aheadfor viewport-ahead loading while the observed sentinel remains in normal document flow; never move the observed target with CSS to fake prefetch distance;- plain CSS/SCSS for appearance;
- shared runtime paths and Axum adapters supplied by
hemx-axum.
Keep domain types, authorization, persistence, routing, sessions, flags,
observability, and transport policy in the application or integration crate.
Keep hemx-core about typed resources and the closed effect/wire contract.
Compose reusable UI from templates, generated components, and ordinary Rust
functions rather than creating a client component framework.
Use advanced layers only when the job proves the need:
- opaque island JavaScript is a leaf adapter for high-frequency local behavior;
- client-local/wasm handlers are explicit opt-ins, not the default state model;
- atoms are addressable bootstrap/sync resources, not a general reactive store;
- SSE/WebSocket transport carries canonical versioned
EffectBatchbytes; it does not define domain policy.
3. Keep truth on the right side of the boundary
The browser DOM, stored HTML patches, and stored EffectBatch values are not
business truth. Keep authoritative state in ordinary application/domain models
and durable providers. Derive UI effects from that state.
For local/offline products, use explicit commands, events, and projections. Replay, deletion, conflict resolution, reconciliation, and export semantics are product contracts; stop when they have not been chosen. For server products, normal HTTP security semantics remain authoritative even when interactions are enhanced.
Expected failures must become typed, local, useful outcomes: generated field errors/focus for validation, a root-scoped error outlet for recoverable request failure, and fail-closed handling for malformed or incompatible responses. Never turn infrastructure failure into a success-looking empty effect.
Prefer a functional core with an imperative shell. Keep validation,
normalization, authorization decisions, command application, state transitions,
and view-model/projection derivation as deterministic functions over explicit
inputs where that is honest. A useful shape is state + command -> outcome or
facts -> view model, with typed errors rather than hidden mutation. This makes
the largest behavior space cheap to unit-test and mutation-test.
Keep HTTP extraction, sessions, database I/O, clocks, randomness/IDs, queues,
push connections, and host capabilities in a thin handler/application shell.
Read their results once, pass ordinary values into the core, persist the returned
outcome, then render generated effects. Pass time, identity, or policy as data
when only the value matters; do not create a trait for every function merely to
mock it. Use a real adapter boundary when ownership or side effects are real.
Purity is a locality tool, not a religion: orchestration and I/O are inherently
effectful, and EffectBatch remains UI output rather than domain state.
4. Scale one pressure at a time
Use this ladder. Enter a stage only when measured load, availability goals, ownership, compliance, or a distinct failure/resource profile requires it.
| Stage | Default shape | Required proof before moving on |
|---|---|---|
| One process | One deployable; app-owned adapters; simplest durable store that meets the product contract | The real product path, restart behavior, backup/recovery needs, and current bottleneck are known |
| Production monolith | One release unit for server, generated output, and runtime asset; external durable database/session providers as required | Integration tests cover auth, persistence, failures, migration, fingerprint mismatch, and rollback/reload behavior |
| Horizontal web tier | Stateless request replicas around shared authoritative providers; process memory is cache/ephemeral only unless affinity and loss semantics are explicit | Load evidence shows replica scale helps; migrations, readiness, draining, cache invalidation, and session/CSRF behavior work across replicas |
| Push/fan-out tier | Server-owned ongoing SSE/WebSocket connections; add a broker/backplane only when updates must cross processes | Reconnect, ordering, duplicate, authorization, backpressure, and replay expectations are explicit and tested at the required level |
| Worker or read-model split | Isolate a measured CPU, latency, queue, or failure domain while the application remains one understandable product | Job ownership, idempotency, timeout, retry, deduplication, observability, and recovery contracts exist |
| Microservices | Split an independently owned bounded capability with its own release/scaling/SLO pressure and explicit data/API/event contract | The boundary removes a demonstrated constraint and its distributed failure modes are cheaper than the monolith |
Prefer vertical resource tuning, query/index fixes, caching with explicit freshness, bounded concurrency, and horizontal replicas before service splitting. A large codebase is a module-boundary problem before it is a network boundary problem.
5. Preserve the hemx boundary across services
When microservices are justified:
- keep hemplate rendering, generated resources, and UI
EffectBatchcreation in the presentation/application edge that owns the page; - exchange typed domain requests, responses, and events across services—not CSS selectors, DOM instructions, raw hemx opcodes, or app-authored JSON versions of the hemx wire format;
- name one authority for each write model and do not let services casually share mutation ownership;
- version service contracts independently, while deploying each hemx server, its generated metadata, build fingerprint, and runtime asset as a compatible release unit;
- preserve end-user credentials, authorization, CSRF, tenancy, deadlines, and trace context explicitly at each real trust boundary;
- make partial failure visible. Define timeout, idempotency, retry, deduplication, ordering, compensation, and replay only where the chosen interaction requires them—never as generic middleware theater.
Do not put a network hop between a handler and its renderer merely to claim
microservices. Do not use EffectBatch as a business event bus or durable event
log.
6. Organize by cohesive product responsibility
Start small; do not pre-create an architecture directory tree. A production app usually needs only these durable seams:
build.rs # hemx-build/global code generation only
src/main.rs # config, concrete providers, process/bootstrap
src/lib.rs # app/router composition and a testable app surface
src/<feature>/ # add only when a feature is already a real seam
templates/app_shell.heml # document shell
templates/<feature>.heml # feature surface
templates/partials/ # genuinely reused or independently swapped pieces
tests/<journey>.rs # process/integration behavior
tests/browser_<journey>.rs # only browser-dependent behavior
Treat that as a responsibility map, not mandatory scaffolding. A small cohesive
app can remain in lib.rs; file count is not architecture. When a feature has
its own state/commands, handlers, rendering, and tests, move that whole seam
together. Keep its domain model, typed input, application operation, handler,
and generated view calls near one another. Do not spread every request across
generic controllers/, services/, repositories/, dto/, and utils/
directories.
Keep main.rs boring and hard to test because it contains almost no policy.
Expose app construction or mounting from lib.rs so integration tests can use
the real router with controlled concrete providers. Put normal routing,
auth/session, persistence, queues, and transport adapters at the app boundary;
do not move them into hemx core or generated template modules. Keep generated
artifacts in the build output rather than copying them into source control.
Put tiny unit tests beside the responsible module, especially around pure
transitions, validation, authorization decisions, and projections. Put
cross-module HTTP, persistence, and process tests in tests/, named for behavior
rather than implementation. Keep browser journeys few and load-bearing. Extract
a shared partial or helper only after actual reuse or independent swap identity
appears.
If a microservice boundary becomes real, that service owns its contract,
provider/migrations, operational entry point, and contract tests; do not mirror
hypothetical services in the source tree first.
7. Prove behavior at the cheapest authoritative boundary
Choose the tool by what must be observed:
| Boundary | Default tool | What it proves |
|---|---|---|
| Domain/state | Rust #[test] / #[tokio::test] |
Parsing, invariants, commands, projections, and failure classes |
| Template/build | Compiler plus hemx-build diagnostics | Real .heml syntax, generated resources/forms, and cross-file references |
| Handler/effect | hemx_test |
Generated targets/handles, rendered partials, form bodies, and effect batches without raw ids |
| Router/integration | Real Axum router, usually tower::ServiceExt, plus concrete test providers |
HTTP status/headers/body, auth, CSRF, sessions, persistence, and malformed requests |
| Static rendered HTML | Rust scraper crate (HTML parser + CSS-selector queries) |
Escaping, semantic structure, links/forms/attributes, and server-rendered fragments without launching a browser |
| Process lifecycle | hemx_test::TestProcess |
Readiness, real sockets, child cleanup, restart, and production-binary behavior |
| Browser runtime | Rust thirtyfour crate (WebDriver client) with the repository-owned browser smoke |
Delegated events, history, focus, polling/revealed bindings, keyed DOM identity, SSE, and recovery |
| Test quality/release | hemx xtask mutation/full-test commands | Mutation resistance and the elected bounded workspace verification path |
scraper is a Rust HTML parsing and CSS-selector library, not a browser or web
framework. Use it when inspecting final server-rendered HTML is enough. It does
not run JavaScript, apply EffectBatch, maintain focus/history, or prove
SSE/runtime behavior. thirtyfour is a Rust WebDriver client crate; use it only
when an actual browser semantic is the subject;
do not turn every handler assertion into a WebDriver journey. In this repository
thirtyfour is the elected Rust browser adapter, so do not add Playwright,
Fantoccini, or another competing browser stack merely from preference. Outside
this repository, preserve the application's existing runner unless a concrete
missing capability justifies migration.
For WebDriver tests, start the real process through the RAII TestProcess
harness, keep selectors in test adapters, and prefer generated-resource or
stable semantic helpers over copied implementation selectors. Browser selector
helpers are not authoring APIs. Avoid sleeps when the runner can wait for the
observable condition.
Match proof to risk:
- Let compilation reject broken templates and generated contracts.
- Unit-test ordinary domain parsing and state transitions without a browser.
- Test handlers through
hemx_testwith useful generated-resource assertions. - Test route/auth/session/CSRF/persistence and wire failure behavior at the app integration boundary.
- Parse static HTML with
scraperonly for facts that do not require runtime execution. - Use focused repository-owned browser smoke for dynamic attributes, navigation/history, keyed reconciliation, polling/revealed behavior, no-reload interaction, and runtime recovery.
- Add compatibility, rolling-deploy, process-restart, multi-replica, load, or fault tests only when the selected scale stage makes those behaviors part of the contract.
In this repository, prefer the stable commands named by AGENTS.md, especially
focused crate tests and cargo run -p hemx-xtask -- test; use
cargo run -p hemx-xtask -- html-examples-smoke for the pattern gallery and
runtime behavior, and the xtask mutation command rather than direct mutest.
Do not substitute a passing literal lowering fixture, static HTML parse, or
mocks-only test for proof at the rendered/runtime consumer boundary.
Refuse fake sophistication
Do not introduce:
- handwritten resource ids, selector retargeting, raw effect constructors, raw registries, runtime opcodes, manual wire parsing, or hard-coded runtime URLs in normal app code;
- a VDOM, client router, general client state graph, expression runtime, per-node listeners, or handwritten JavaScript for ordinary forms/lists/swaps;
- core-owned auth, sessions, persistence, routing, multipart, transport, sync, analytics, flags, or provider policy;
- a generic repository/service/provider interface before a concrete second use or required external contract exists;
- microservices by entity name, team aspiration, file count, or hypothetical scale;
- shared process memory presented as durable or cross-replica state;
- retries, caches, queues, brokers, sagas, event sourcing, CQRS, Kubernetes, or a service mesh without a named failure/load contract and verification path.
“Suckless” here means fewer authorities and mechanisms, not fewer safety checks. The elegant design keeps HTML semantics, typed boundaries, explicit ownership, and failure truth while deleting accidental layers.
Handoff
For architecture or review requests, report compactly:
- the user-visible path and current bottleneck/risk;
- the selected scale stage and why the previous stage is insufficient;
- state, rendering, transport, and service ownership;
- the smallest end-to-end change;
- complexity explicitly refused;
- proof run and any unresolved provider/product contract.
For implementation, make that slice reachable and verify it; do not leave a “scalable” abstraction that no real path uses.