clockworklabs/SpacetimeDB · error

environment-only publication requires…

Error message

environment-only publication requires expected_module_version

What it means

The core's `update_module_host` validates that an environment-only publication (identified by an empty `program_bytes`) always carries an `expected_module_version` hash. This hash implements optimistic concurrency: the caller asserts which module version it read the environment for, preventing the environment update from racing with a concurrent module update. Without it, the core refuses the update via `ensure!`.

Solutions

  1. Fetch the database's current module version/hash first and pass it as `expected_module_version`.
  2. If you actually need code changes, supply non-empty `program_bytes` so this is a full module update.
  3. Update the calling client/CLI to a version that includes the module hash in environment-only updates.
  4. Re-read the database state immediately before the call to get a fresh hash and avoid optimistic-concurrency conflicts.

Example fix

// before
host_controller.update_module_host(db, host_type, Vec::new(), policy, env_update, None).await?
// after
let expected = get_current_module_hash(db).await?; // e.g. from database metadata
host_controller.update_module_host(db, host_type, Vec::new(), policy, env_update, Some(expected)).await?
Defensive patterns

Strategy: validation

Validate before calling

// rust caller-side check mirroring the core rule
anyhow::ensure!(
    !program_bytes.is_empty() || expected_module_version.is_some(),
    "environment-only update requires expected_module_version"
);

Try / catch

// rust
if let Err(e) = update_module_host(..., Vec::new(), policy, env, None).await {
    if e.to_string().contains("environment-only publication requires") {
        let hash = fetch_current_module_hash(db).await?;
        update_module_host(..., Vec::new(), policy, env, Some(hash)).await?;
    } else { return Err(e); }
}

Prevention

When it happens

Trigger: Calling `update_module_host` (via the update/publish database path) with empty `program_bytes` (environment-only update) and `expected_module_version: None`; a CLI/server path that constructs an `EnvironmentUpdate` without first fetching the current module hash.

Common situations: Tooling or scripts updating only environment variables for a published database but skipping the step of reading the current module version/hash; API clients written before the expected_module_version requirement was introduced.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-09-20). Data as JSON: /api/errors/715b4f637f40baf4. Report an issue: GitHub.

Appendix: source

Thrown at crates/core/src/host/host_controller.rs:606

    /// [`Self::get_or_launch_module_host`] for details on what this entails).
    ///
    /// If the host was running, and the update fails, the previous version of
    /// the host keeps running.
    #[tracing::instrument(level = "trace", skip_all, err)]
    #[allow(clippy::too_many_arguments)]
    pub async fn update_module_host(
        &self,
        database: Database,
        host_type: HostType,
        replica_id: u64,
        program_bytes: Box<[u8]>,
        policy: MigrationPolicy,
        environment: spacetimedb_lib::environment::EnvironmentUpdate,
        expected_module_version: Option<spacetimedb_lib::Hash>,
    ) -> anyhow::Result<UpdateDatabaseResult> {
        environment.validate()?;
        let environment_only = program_bytes.is_empty();
        anyhow::ensure!(
            !environment_only || expected_module_version.is_some(),
            "environment-only publication requires expected_module_version"
        );
        let program = Program::from_bytes(host_type.into(), program_bytes);
        trace!(
            "update module host {}/{}: genesis={} update-to={}",
            database.database_identity,
            replica_id,
            database.initial_program,
            program.hash
        );

        let Ok(mut guard) = self.acquire_write_lock(replica_id).await else {
            bail!("unable to lock database {} for update", database.database_identity);
        };

        // `HostController::clone` is fast,
        // as all of its fields are either `Copy` or wrapped in `Arc`.

View on GitHub (pinned to eddf9f5014)