Skip to content

Concepts

Multi-tenancy

Available since: v3.0.0

Multi-tenancy allows 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.

use surrealdb::Surreal;
use surrealdb::engine::local::Mem;
use surrealdb::types::{ToSql, Value};

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    let db = 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());
    let new = 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.

use std::sync::OnceLock;
use surrealdb::engine::any::connect;
use surrealdb::{Surreal, engine::any::Any};

static DB: OnceLock<Surreal<Any>> = OnceLock::new();

async fn new_with(namespace: &str, database: &str) -> Surreal<Any> {
    let db = DB.get().unwrap().clone();
    db.use_ns(namespace).use_db(database).await.unwrap();
    db
}

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    // Set the overall connection
    DB.set(connect("memory").await?).unwrap();

    // NS and DB must now be specifically indicated to use
    let session1 = new_with("acme", "app").await;
    let session2 = new_with("user", "app").await;
    Ok(())
}

Before version 3.0, cloning a Surreal<C> created a cheap clone of the same session. Where .clone() is used to pass a Surreal<C> around and multi-tenancy is not wanted, wrap the client in a type like an Arc so that only the wrapper is cloned.

What that saves depends on the engine. An embedded engine builds the new session in memory, which is what the example below measures. A remote engine replays the session setup - the attach, the sign-in, and the USE NS and USE DB calls - across the connection for each clone, so every clone costs a series of network round trips rather than a copy, and the replays are written to the socket ahead of queries that are already waiting on it.

Web framework state is where this can add up. A router stores its state by cloning it into every route, and an extractor clones it again on each request, so a bare Surreal<C> passed to framework functions such as Axum's with_state, Actix's app_data or Rocket's manage opens a session per route and another per request. Wrap the client in an Arc first, or hold it in a static as the framework guides do.

use std::sync::Arc;
use std::time::Instant;

use surrealdb::Surreal;
use surrealdb::engine::local::Mem;

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    let db = Surreal::new::<Mem>(()).await?;
    db.use_ns("ns").use_db("db").await?;

    let now = Instant::now();
    for _ in 0..100 {
        let cloned = db.clone();
        cloned
            .query(
                "
            LET $one = CREATE ONLY person;
            LET $two = CREATE ONLY person;
            RELATE $one->likes->$two;
        ",
            )
            .await?;
    }
    println!("Elapsed: {:?}", now.elapsed());

    let arced = Arc::new(db);
    let now = Instant::now();
    for _ in 0..100 {
        let cloned_arc = Arc::clone(&arced);
        cloned_arc
            .query(
                "
            LET $one = CREATE ONLY person;
            LET $two = CREATE ONLY person;
            RELATE $one->likes->$two;
        ",
            )
            .await?;
    }
    println!("Elapsed: {:?}", now.elapsed());

    Ok(())
}

Let's now take a look at some usage examples that take advantage of the new multi-tenancy available in SurrealDB 3.0.

This first example shows how to use multi-session support to implement multi-tenancy, where each tenant operates in their own isolated namespace:

use surrealdb::Surreal;
use surrealdb::engine::local::Mem;
use surrealdb::opt::Resource;
use surrealdb::types::{RecordId, SurrealValue, object};

#[derive(Debug, SurrealValue)]
struct User {
    id: RecordId,
    name: String,
}

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    // Create the base database connection
    let db = Surreal::new::<Mem>(()).await?;

    // Tenant 1: ACME Corporation
    let acme_db = db.clone();
    acme_db.use_ns("acme").use_db("app").await?;
    acme_db
        .create(Resource::from(("user", "john")))
        .content(object! { name: "John from ACME" })
        .await?;

    // Tenant 2: Widget Inc
    let widget_db = db.clone();
    widget_db.use_ns("widget").use_db("app").await?;
    widget_db
        .create(Resource::from(("user", "john")))
        .content(object! { name: "John from Widget Inc" })
        .await?;

    // Tenant 3: Example LLC
    let example_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
    let acme_users: Vec<User> = acme_db.select("user").await?;
    println!("ACME users: {acme_users:?}");
    // Output: [User { id: user:john, name: "John from ACME" }]

    let widget_users: Vec<User> = widget_db.select("user").await?;
    println!("Widget users: {widget_users:?}");
    // Output: [User { id: user:john, name: "John from Widget Inc" }]

    // Tenants can operate concurrently without interfering with each other
    let acme_alice = acme_db
        .create(Resource::from(("user", "alice")))
        .content(object! { name: "Alice from ACME" });
    let widget_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:

use surrealdb::Surreal;
use surrealdb::engine::local::Mem;
use surrealdb::types::Decimal;

#[tokio::main]
async fn main() -> surrealdb::Result<()> {
    // Create the base database connection
    let db = Surreal::new::<Mem>(()).await?;

    // Database 1: North America sales
    let na_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
    let eu_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
    let asia_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 (mut na_result, mut eu_result, mut asia_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)) }"),
    )?;

    let na_total = na_result
        .take::<Option<Decimal>>("total")?
        .unwrap_or_default();
    let eu_total = eu_result
        .take::<Option<Decimal>>("total")?
        .unwrap_or_default();
    let asia_total = asia_result
        .take::<Option<Decimal>>("total")?
        .unwrap_or_default();

    println!("North America total: ${na_total:.2}"); // $4500.00
    println!("Europe total: ${eu_total:.2}"); // $3000.00
    println!("Asia total: ${asia_total:.2}"); // $7300.00
    println!("Grand total: ${:.2}", na_total + eu_total + asia_total); // $14800.00

    // 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 (mut na_result, mut eu_result, mut asia_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)) }"
        ),
    )?;

    println!("\nWith regional discounts:");
    let na_disc = na_result
        .take::<Option<Decimal>>("total")?
        .unwrap_or_default();
    let eu_disc = eu_result
        .take::<Option<Decimal>>("total")?
        .unwrap_or_default();
    let asia_disc = asia_result
        .take::<Option<Decimal>>("total")?
        .unwrap_or_default();

    println!("North America (10% off): ${na_disc:.2}"); // $4050.00
    println!("Europe (15% off): ${eu_disc:.2}"); // $2550.00
    println!("Asia (5% off): ${asia_disc:.2}"); // $6935.00

    Ok(())
}

Was this page helpful?