diff --git a/README.md b/README.md index bac123f..188b406 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ and integrate at explicit boundaries. req: laws/002 req: auth/001 fields and credentials semantics. See `docs/recipes/auth-session-csrf.md`. req: auth/004 req: auth/005 - **Observability, feature flags, killswitches, deploy:** use explicit platform integrations around handlers, routes, and runtime assets. Core hemx must not - vendor providers or add framework-specific magic. + vendor providers or add framework-specific magic. See `docs/recipes/deploy-versioning.md`. - **PWA/offline/sync:** optional adapters may reuse generated targets/effects, but core hemx must not gain a mandatory client state graph or local app runtime. req: canonical_authoring/008 @@ -104,7 +104,7 @@ The generated API, symbols, effect wire schema, and JavaScript runtime carry schema/ABI versions. Deploy a matching server, generated output, and runtime asset together. Build fingerprints are derived from the generated surface and ABI parts; the runtime refuses incompatible effect responses and integrations should -fall back to a full page reload when possible. req: abi/001 req: abi/002 req: failure/005 +fall back to a full page reload when possible. See `docs/recipes/deploy-versioning.md`. req: abi/001 req: abi/002 req: failure/005 Before a v1 release, the semver policy and upgrade notes should explicitly state which surfaces are stable: beginner generated helpers and handler shapes; the diff --git a/docs/recipes/deploy-versioning.md b/docs/recipes/deploy-versioning.md new file mode 100644 index 0000000..bfe3ec4 --- /dev/null +++ b/docs/recipes/deploy-versioning.md @@ -0,0 +1,144 @@ +# Recipe: deploy and version compatibility + +This recipe describes the production deployment boundary for a hemx app. The +server binary, generated Rust helpers, generated symbol/fingerprint metadata, and +JavaScript runtime asset must be treated as one release unit. hemx core provides +the ABI/fingerprint checks; the application and platform own rollout, caching, +observability, and rollback policy. req: abi/001 req: abi/002 req: runtime/004 + +Use this for `examples/saas`-style apps before putting multiple app versions +behind a load balancer or CDN. + +## Release unit + +A compatible release contains: + +- the Rust server binary built from the same checkout as `build.rs` +- generated `hemx.generated.rs` and symbols produced during that build +- the `hemx-js` runtime asset served by that server or deployed with the same + release +- templates, CSS, island JavaScript, migrations, and app config for that release + +Do not mix a newly built server with an old `/hemx.js`, old generated output, or +old cached page shell. Build fingerprints are derived from Surface/schema/ABI +parts, so mismatches are detected and partial updates fail closed instead of +mutating the wrong DOM. req: abi/003 req: abi/004 req: failure/005 + +## Asset serving + +Serve `/hemx.js` from the same release as the server when possible: + +```rust +async fn runtime() -> impl IntoResponse { + hemx_axum::runtime_js() +} +``` + +If a CDN is required, publish the runtime under a content-addressed release path +and keep the page shell pointing at that exact path: + +```html + +``` + +Avoid long-lived unversioned CDN caching for `/hemx.js`. If an unversioned path +is used, set a short cache TTL or purge it atomically with the server rollout. +CSS and explicit island scripts should follow the same release path policy. + +## Rolling deploys + +Rolling deploys are safe when every response serves a self-consistent release. +The easiest policy is sticky-by-release routing: + +- page HTML, interaction POSTs, SSE/polling endpoints, and `/hemx.js` come from + the same server revision +- a load balancer cookie or platform routing key keeps an active browser on one + revision during the rollout window +- old revisions stay alive until active SSE connections and in-flight forms have + drained + +If sticky routing is not available, make the mismatch behavior user-safe: + +- keep full page GETs compatible across one adjacent version when practical +- allow interaction responses to fail closed on fingerprint mismatch +- prefer redirect/reload fallback over best-effort partial mutation +- report mismatch counts so rollouts can be paused quickly + +The runtime must not grow a negotiation protocol or compatibility shim in core; +capability negotiation belongs to optional integration crates. req: runtime/004 + +## Fingerprint and mismatch behavior + +Initial roots carry the build fingerprint, and effect responses carry the +fingerprint for the batch. The runtime compares them before applying effects. +On mismatch, the app should recover by reloading or navigating to a full page +owned by the current server revision. req: abi/003 req: abi/004 + +Recommended app behavior: + +```text +fingerprint mismatch + -> record metric: hemx.fingerprint_mismatch + -> show a short-lived "Updating…" notice if possible + -> perform full page reload/navigation +``` + +Never ignore a mismatch to preserve a partial update. Resource ids are stable +within a build and best-effort across compatible symbol paths, but they are not a +persistence or cross-version addressing contract. req: abi/005 + +## Semver policy for v1 apps + +For v1, document changes in three buckets: + +- **Beginner API:** generated helpers, `#[hemx::app]`, `#[hemx::component]`, + `#[hemx::handler]`, `#[hemx::form]`, `hemx::page`, tuple `IntoEffect`, and + `Result` mapping. Breaking changes require a major version + or an explicit migration note. +- **Wire/runtime ABI:** EffectBatch schema, runtime ABI version, and fingerprint + inputs. Incompatible changes must bump ABI versions and fail closed at runtime. +- **Advanced escape hatches:** raw effects, manual registries, low-level ids, + raw render/target construction, runtime hooks, SSE internals, and island + internals. These may evolve faster, but must remain named as advanced and must + not leak into beginner docs. req: public_api/002 req: public_api/005 + +Upgrade notes should explain what changed, whether generated code must be +regenerated, whether `/hemx.js` must be rolled with the server, and what fallback +users see if an old page talks to a new server. + +## Deployment checklist + +Before promoting a release: + +```sh +cargo run -p hemx-xtask -- test +cargo check --workspace +redgate health --strict +``` + +Then verify deployment-specific behavior: + +- page HTML includes the intended `/hemx.js`, CSS, and island asset release paths +- interaction endpoints return the same build fingerprint as the initial root +- SSE/polling endpoints stream batches from the same revision +- a stale page talking to the new server reloads or navigates instead of applying + a partial update +- fingerprint mismatch metrics/logs are visible to the platform team +- rollback serves a self-consistent old server/runtime pair + +These checks belong in the app/platform pipeline. hemx should provide the small +runtime handshake and clear failure boundary, not a deployment platform. + +## Observability hooks + +Track deployment compatibility as app/platform metrics: + +- `hemx.fingerprint_mismatch` +- `hemx.effect_decode_error` +- `hemx.missing_target` +- `hemx.sse_reconnect` +- `hemx.full_reload_fallback` + +The metric names are suggestions, not core API. The important behavior is that a +team can see mismatches, pause a rollout, and recover with a full page response +without weakening the runtime's tiny, selectorless contract. req: failure/001 req: failure/005 diff --git a/examples/saas/README.md b/examples/saas/README.md index 30b71a5..b76cddb 100644 --- a/examples/saas/README.md +++ b/examples/saas/README.md @@ -12,7 +12,7 @@ What it proves today: - page shell with plain CSS and one explicit metrics island script - compile-time surface generation plus interaction tests -For provider-explicit boundaries, see `../../docs/recipes/sqlx-persistence.md` and `../../docs/recipes/auth-session-csrf.md`. +For provider-explicit boundaries, see `../../docs/recipes/sqlx-persistence.md`, `../../docs/recipes/auth-session-csrf.md`, and `../../docs/recipes/deploy-versioning.md`. What it deliberately does not claim yet: