Multi-tenancy was introduced in SurrealDB 3.0, allowing each tenant to operate inside its own isolated namespace and database.
Multi-session inside the Rust SDK is implemented through a session cloning mechanism. When you clone a Surreal<C> client instance, it creates a new session with independent state while sharing the underlying database connection. Sessions share the same physical connection for efficiency, are thread-safe and can be used concurrently across Tokio tasks, and work with all connection types (embedded and remote).
Each cloned instance maintains its own:
Namespace and database selection (use_ns/use_db)
Authentication state (signin/signup/invalidate)
Session variables (set/unset)
Transactions (begin/commit/cancel)
Note that these will remain unchanged when cloning a connection.
#[tokio::main] asyncfnmain()->surrealdb::Result<()> { letdb=Surreal::new::<Mem>(()).await?; db.use_ns("ns").use_db("db").await?; db.set("val",1).await?; println!("$val is `{}`",db.query("$val").await?.take::<Value>(0).unwrap().to_sql()); letnew=db.clone(); println!("$val is still `{}` in the new session",new.query("$val").await?.take::<Value>(0).unwrap().to_sql()); // Set to a different value inside 'new' new.set("val",100).await?;
println!("$val is still `{}` in the original",db.query("$val").await?.take::<Value>(0).unwrap().to_sql()); println!("But is now `{}` in the new session",new.query("$val").await?.take::<Value>(0).unwrap().to_sql()); Ok(()) }
If a connection without any set values is desired, using a static singleton along with a convenience function is one way to achieve this.
asyncfnnew_with(namespace: &str,database: &str)->Surreal<Any> { letdb=DB.get().unwrap().clone(); db.use_ns(namespace).use_db(database).await.unwrap(); db }
#[tokio::main] asyncfnmain()->surrealdb::Result<()> { // Set the overall connection DB.set(connect("memory").await?).unwrap();
// NS and DB must now be specifically indicated to use letsession1=new_with("acme","app").await; letsession2=new_with("user","app").await; Ok(()) }
Before version 3.0, cloning a Surreal<C> would create a cheap clone of the same session. If you have been using .clone() to pass on a Surreal<C> without a need for multi-tenancy, it is now preferable to wrap the client inside a type like an Arc to ensure that only the wrapper is cloned. Doing so will be somewhat more performant, as this example shows.
// Tenant 3: Example LLC letexample_db=db.clone(); example_db.use_ns("example").use_db("app").await?; example_db .create(Resource::from(("user","john"))) .content(object!{name: "John from Example LLC"}) .await?;
// Each tenant sees only their own data letacme_users: Vec<User> =acme_db.select("user").await?; println!("ACME users: {acme_users:?}"); // Output: [User { id: user:john, name: "John from ACME" }]
// Tenants can operate concurrently without interfering with each other letacme_alice=acme_db .create(Resource::from(("user","alice"))) .content(object!{name: "Alice from ACME"}); letwidget_alice=widget_db .create(Resource::from(("user","alice"))) .content(object!{name: "Alice from Widget"}); tokio::try_join!(acme_alice,widget_alice)?;
Ok(()) }
The next example demonstrates querying across multiple databases simultaneously to aggregate data:
#[tokio::main] asyncfnmain()->surrealdb::Result<()> { // Create the base database connection letdb=Surreal::new::<Mem>(()).await?;
// Database 1: North America sales letna_db=db.clone(); na_db.use_ns("company").use_db("sales_na").await?; na_db .query(r#" CREATE sale:1 SET amount = 1000.00dec, region = "North America"; CREATE sale:2 SET amount = 1500.00dec, region = "North America"; CREATE sale:3 SET amount = 2000.00dec, region = "North America"; "#) .await? .check()?;
// Database 2: Europe sales leteu_db=db.clone(); eu_db.use_ns("company").use_db("sales_eu").await?; eu_db .query(r#" CREATE sale:1 SET amount = 1200.00dec, region = "Europe"; CREATE sale:2 SET amount = 1800.00dec, region = "Europe"; "#) .await? .check()?;
// Database 3: Asia sales letasia_db=db.clone(); asia_db.use_ns("company").use_db("sales_asia").await?; asia_db .query(r#" CREATE sale:1 SET amount = 3000.00dec, region = "Asia"; CREATE sale:2 SET amount = 2500.00dec, region = "Asia"; CREATE sale:3 SET amount = 1800.00dec, region = "Asia"; "#) .await? .check()?;
// Query all databases concurrently let(mutna_result,muteu_result,mutasia_result)=tokio::try_join!( na_db.query("RETURN { total: math::sum((SELECT VALUE amount FROM sale)) }"), eu_db.query("RETURN { total: math::sum((SELECT VALUE amount FROM sale)) }"), asia_db.query("RETURN { total: math::sum((SELECT VALUE amount FROM sale)) }"), )?;
// You can also perform complex operations on specific databases // while maintaining independent session variables na_db.set("discount_rate",0.1).await?; eu_db.set("discount_rate",0.15).await?; asia_db.set("discount_rate",0.05).await?;
let(mutna_result,muteu_result,mutasia_result)=tokio::try_join!( na_db.query( "RETURN { total: math::sum((SELECT VALUE amount * (1 - $discount_rate) FROM sale)) }" ), eu_db.query( "RETURN { total: math::sum((SELECT VALUE amount * (1 - $discount_rate) FROM sale)) }" ), asia_db.query( "RETURN { total: math::sum((SELECT VALUE amount * (1 - $discount_rate) FROM sale)) }" ), )?;