clockworklabs/SpacetimeDB · error · NodesError

environment access requires a host-dispatched root module…

Error message

environment access requires a host-dispatched root module function

What it means

env_get reads configuration values from the st_env system table using the calling module's schema. To keep environment reads consistent and permissioned, SpacetimeDB only allows them from a host-dispatched, non-namespaced (root) module function — i.e. a real reducer, not an internal/inline or namespaced helper. Calling env_get outside that context raises this wrapped DBError.

Solutions

  1. Move the env_get call into a top-level reducer (a non-namespaced module function dispatched by the host).
  2. Pass the value obtained in the reducer as an argument to any namespaced helper that needs it.
  3. Validate the key first with spacetimedb_lib::environment::validate_key to avoid the separate InvalidEnvironmentKey error.

Example fix

// before: namespaced helper reads env itself
mod internal { pub fn read_key(env: &InstanceEnv, k: &str) { env.env_get(k); } }

// after: reducer reads env, passes value down
#[reducer]
fn apply_config(ctx: &ReducerContext) {
    let v = ctx.env_get("API_KEY");
    internal::apply(v);
}
Defensive patterns

Strategy: validation

Validate before calling

// call env_get only from a root reducer context
assert!(ctx.func_name().map_or(false, |n| !n.is_namespaced()), "env_get requires root reducer");

Try / catch

match env.env_get(key) { Err(NodesError::..) => log::error("env access outside reducer context"), other => other }

Prevention

When it happens

Trigger: Calling env_get (directly or via a read of the st_env table / config API) from a namespaced function (func name is_namespaced() is true), from no function context (func_name None), or when environment_call_active is false, e.g. from an HTTP/SQL code path rather than a dispatched reducer.

Common situations: Reading env config inside a scheduled procedure or namespaced module helper; attempting env access from a view or external API handler instead of a reducer; calling env_get before the host sets up the call context.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

Thrown at crates/core/src/host/instance_env.rs:317

    pub fn in_tx(&self) -> bool {
        self.get_tx().is_ok()
    }

    pub(crate) fn take_tx(&self) -> Result<MutTxId, GetTxError> {
        self.tx.take()
    }

    pub(crate) fn relational_db(&self) -> &Arc<RelationalDB> {
        self.replica_ctx.relational_db()
    }

    /// Read configuration using the schema of this exact module instance.
    /// Missing optional reads also register a view dependency on `st_env`.
    pub(crate) fn env_get(&self, key: &str) -> Result<Option<String>, NodesError> {
        use spacetimedb_datastore::system_tables::ST_ENV_ID;
        spacetimedb_lib::environment::validate_key(key).map_err(|_| NodesError::InvalidEnvironmentKey)?;
        if !self.environment_call_active || self.func_name.as_ref().is_none_or(|name| name.is_namespaced()) {
            return Err(DBError::Other(anyhow::anyhow!(
                "environment access requires a host-dispatched root module function"
            ))
            .into());
        }
        if let Ok(mut tx) = self.get_tx() {
            tx.record_table_scan(&self.func_type, ST_ENV_ID);
            return self.read_declared_environment(&*tx, key);
        }
        if !matches!(self.func_type, FuncCallType::Procedure) {
            return Err(NodesError::NotInTransaction);
        }
        self.relational_db()
            .with_read_only(Workload::Internal, |tx| self.read_declared_environment(tx, key))
    }

    fn read_declared_environment(&self, state: &impl StateView, key: &str) -> Result<Option<String>, NodesError> {
        use spacetimedb_datastore::system_tables::{StModuleFields, ST_MODULE_ID};
        let fail = |message| NodesError::from(DBError::Other(anyhow::anyhow!("{message}")));

View on GitHub (pinned to eddf9f5014)