clockworklabs/SpacetimeDB · error

AddRowLevelSecurity: RLS `{sql_rls}` not found in new module

Error message

AddRowLevelSecurity: RLS `{sql_rls}` not found in new module def

What it means

Adding a row-level-security policy: the step carries the RLS SQL text itself as its key, and plan.new.lookup::<RawRowLevelSecurityDefV9>(sql_rls) returns None - the new def set has no RLS definition registered under exactly that SQL string. Any drift in the SQL text (formatting, quoting, normalization) or absence of the RLS in the applied defs makes the lookup miss. The publish aborts.

Source

Thrown at crates/engine/src/update.rs:602

            spacetimedb_schema::auto_migrate::AutoMigrateStep::ChangePrimaryKey(table_name_key) => {
                let (namespace, local) = table_name_key;
                let table_name = joined(namespace, local);
                let (_owning_def, table_def) = plan.new.find_table(table_name_key).ok_or_else(|| {
                    anyhow::anyhow!("ChangePrimaryKey: table `{table_name}` not found in new module def")
                })?;
                log!(logger, "Changing primary key for table `{table_name}`");
                stdb.alter_table_primary_key(tx, &table_name, table_def.primary_key)?;
            }
            spacetimedb_schema::auto_migrate::AutoMigrateStep::AddSchedule(_) => {
                anyhow::bail!("Adding schedules is not yet implemented");
            }
            spacetimedb_schema::auto_migrate::AutoMigrateStep::RemoveSchedule(_) => {
                anyhow::bail!("Removing schedules is not yet implemented");
            }
            spacetimedb_schema::auto_migrate::AutoMigrateStep::AddRowLevelSecurity(sql_rls) => {
                log!(logger, "Adding row-level security `{sql_rls}`");
                let rls = plan.new.lookup::<RawRowLevelSecurityDefV9>(sql_rls).ok_or_else(|| {
                    anyhow::anyhow!("AddRowLevelSecurity: RLS `{sql_rls}` not found in new module def")
                })?;
                let rls = RowLevelExpr::build_row_level_expr(tx, &auth_ctx, rls)?;

                stdb.create_row_level_security(tx, rls.def)?;
            }
            spacetimedb_schema::auto_migrate::AutoMigrateStep::RemoveRowLevelSecurity(sql_rls) => {
                log!(logger, "Removing row-level security `{sql_rls}`");
                stdb.drop_row_level_security(tx, sql_rls.clone())?;
            }
            spacetimedb_schema::auto_migrate::AutoMigrateStep::AddColumns(table_name_key) => {
                let (namespace, local) = table_name_key;
                let table_name = joined(namespace, local);
                let (owning_def, table_def) = plan
                    .new
                    .find_table(table_name_key)
                    .ok_or_else(|| anyhow::anyhow!("AddColumns: table `{table_name}` not found in new module def"))?;
                let table_id = stdb.table_id_from_name_mut(tx, &table_name).unwrap().unwrap();
                let column_schemas = column_schemas_from_defs(owning_def, &table_def.columns, table_id);

View on GitHub (pinned to 6dee26c6ef)

Solutions

  1. Add or change the RLS policy in a dedicated publish with no other schema edits.
  2. Keep the SQL string byte-identical between publishes when you do not intend to change it.
  3. Clean-rebuild to eliminate def/plan desync, then republish.
  4. Align SDK/compiler versions between builds.
  5. Dev: spacetime publish --delete-data <db>.
  6. Report if a clean single-change publish still fails.

Example fix

// before: RLS SQL edited together with other schema changes in one publish
#[spacetimedb::table(row_level_security = "SELECT * FROM t WHERE owner = :sender")]
// ... plus renames/column edits ...

// after: two publishes
// 1) schema changes only, RLS untouched
// 2) update row_level_security alone
Defensive patterns

Strategy: validation

Validate before calling

use spacetimedb_schema::auto_migrate::AutoMigrateStep;
use spacetimedb_schema::raw_def::v9::RawRowLevelSecurityDefV9;
for step in &plan.steps {
    if let AutoMigrateStep::AddRowLevelSecurity(sql) = step {
        if plan.new.lookup::<RawRowLevelSecurityDefV9>(sql).is_none() {
            anyhow::bail!("RLS with SQL {sql:?} not registered in the new def set");
        }
    }
}

Type guard

fn rls_registered(plan: &AutoMigratePlan, sql: &str) -> bool {
    plan.new.lookup::<RawRowLevelSecurityDefV9>(sql).is_some()
}

Try / catch

match update_database(&stdb, tx, &plan, ...).await {
    Err(e) if e.to_string().contains("AddRowLevelSecurity")
        && e.to_string().contains("not found in new module def") => {
        // RLS SQL key drift: republish the RLS change alone, SQL byte-identical elsewhere.
    }
    result => result?,
}

Prevention

When it happens

Trigger: Editing the RLS SQL text (even whitespace or quote style) while other schema changes share the same publish; the RLS present in the planner's input defs but absent or textually different in the def set actually applied; macro serialization differences across SDK versions.

Common situations: Iterating on RLS expressions alongside table changes; upgrading SDK versions that serialize or normalize RLS SQL differently; bundling RLS edits with renames.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@6dee26c6ef (2026-08-20). Data as JSON: /api/errors/4c702d2d9ab5da29. Report an issue: GitHub.