clockworklabs/SpacetimeDB · error

database ownership changed before deletion

Error message

database ownership changed before deletion

What it means

`delete_database` verifies that the identity requesting deletion still matches the database's stored `owner_identity` before removing it from the control DB. This TOCTOU guard prevents a caller from deleting a database whose ownership was transferred (or re-created under a different owner) between lookup and deletion. If ownership changed, deletion is refused instead of letting a non-owner destroy the database.

Solutions

  1. Check `database.owner_identity` via the CLI/API (`spacetime sql` on the database metadata or inspect the publish response) and delete using the identity that currently owns the database.
  2. Re-authenticate with the credentials of the current owner instead of the original publisher.
  3. If ownership transfer is intended, complete/verify the transfer first, then retry deletion with the new owner identity.
  4. If the database identity is ambiguous, confirm you are targeting the correct database identity rather than another owner's database.

Example fix

// before
client.delete_database(caller_identity = identityA, database_identity)
// after (ownership moved to identityB)
client.delete_database(caller_identity = database.owner_identity /* identityB */, database_identity)
Defensive patterns

Strategy: validation

Validate before calling

// before deleting
let database = control_db.get_database_by_identity(&database_identity)?;
if let Some(db) = database {
    if db.owner_identity != caller_identity {
        // skip or re-authenticate as the current owner
        return Err(anyhow!("cannot delete: owned by {}", db.owner_identity));
    }
}

Type guard

fn can_delete(db: &Database, caller: &Identity) -> bool { db.owner_identity == *caller }

Prevention

When it happens

Trigger: Calling `delete_database(caller_identity, database_identity)` when `database.owner_identity != caller_identity` — i.e. the database was transferred to another owner, re-published by a different identity, or the caller is passing the wrong caller identity / wrong database identity.

Common situations: A developer publishes a database under identity A, transfers or re-publishes it under identity B, then an automation/CI job still holding A's credentials tries to delete it; or a shared test harness (e.g. `finish_module_test`) deletes databases with a stale or different identity than the one used at publish time.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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

Appendix: source

Thrown at crates/standalone/src/lib.rs:433

                        spec.host_type,
                        host.replica_id,
                        spec.program_bytes.to_vec().into(),
                        style,
                    )
                    .await
            }
            None => anyhow::bail!(
                "Database `{}` does not exist",
                spec.database_identity.to_abbreviated_hex()
            ),
        }
    }

    async fn delete_database(&self, caller_identity: &Identity, database_identity: &Identity) -> anyhow::Result<()> {
        let Some(database) = self.control_db.get_database_by_identity(database_identity)? else {
            return Ok(());
        };
        anyhow::ensure!(
            database.owner_identity == *caller_identity,
            "database ownership changed before deletion"
        );
        self.control_db.delete_database(database.id)?;

        for instance in self.control_db.get_replicas_by_database(database.id)? {
            self.delete_replica(instance.id).await?;
        }

        Ok(())
    }

    async fn reset_database(&self, caller_identity: &Identity, spec: DatabaseResetDef) -> anyhow::Result<()> {
        let previous = self
            .control_db
            .get_database_by_identity(&spec.database_identity)?
            .with_context(|| format!("Database `{}` does not exist", spec.database_identity))?;
        anyhow::ensure!(

View on GitHub (pinned to eddf9f5014)