Files
hemx/docs/recipes/deploy-versioning.md
T

6.0 KiB

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 runtime asset, 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 the embedded runtime at the helper-provided fingerprinted path from the same release as the server:

use axum::{routing::get, Router};
use hemx_axum::{runtime_js, runtime_js_path};

let app = Router::new().route(runtime_js_path(), get(runtime));

async fn runtime() -> impl axum::response::IntoResponse {
    runtime_js()
}

Render page shells with that same runtime_js_path() value:

<script +src="self.runtime_src" defer></script>

runtime_js() is safe for long-lived caching because the public path includes a hash of the embedded runtime bytes and the response carries immutable cache headers. Do not publish app-owned version query strings or a long-lived unversioned runtime URL. 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 the runtime_js_path() asset 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:

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], generated page-boundary rendering, tuple IntoEffect, and Result<impl IntoEffect, E> 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 the helper-provided runtime asset must be rolled with the server, and what fallback users see if an old page talks to a new server. Use docs/versioning.md as the release-policy checklist.

Deployment checklist

Before promoting a release:

cargo run -p hemx-xtask -- test
cargo check --workspace
redgate refs

Then verify deployment-specific behavior:

  • page HTML includes the intended runtime_js_path(), 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