# 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:
```rust
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:
```html
```
`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:
```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]`, generated page-boundary rendering, 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 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:
```sh
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