Turso is joining Supabase to give every agent its own database Read the news →
Guest Post

Toasty now fully supports Turso

Toasty, the async Rust ORM, now supports Turso in all three modes: local files with concurrent writes, local-first sync, and fully remote over HTTP.

Cover image for Toasty now fully supports Turso

You all have seen the news: DHH declared loud and clear in the Ruby on Rails keynote: Now that AI writes all the code, Rust is everywhere. (I disagree with him that Rust code is ugly, but let's not go there!)

This is especially close to my heart: In the early days of my career, I was myself a long-term contributor to Ruby on Rails. I always loved the simplicity, the guardrails, and the "let's get it done" attitude of the framework.

For the Rust community, my claim to fame was that I have later built the Tokio executor for Rust. I now have hopes to extend that work and bring some of the Rails energy to the Rust world. To achieve that, I have been building two projects: topcoat, a batteries-included framework for building web apps, and toasty, an async ORM that aims to provide ergonomic access to the most used databases out there.

RoR is big on SQLite, and deservedly so: with an unmatched local experience, SQLite allows every RoR app to hit the ground running without any extra setup. With Toasty, we are going one step further: we are partnering with Turso to provide the same experience, but building on a modern version of SQLite that offers new features like concurrent writes, sync, etc.

In this article I'll walk you through the basics of how to make the most out of Turso on Toasty.

#The basics

Toasty supports 3 modes for Turso:

  • fully local
  • sync, with an external sync server
  • remote, with direct access to the Turso Cloud

The goal is that aside from the constructors, everything should feel the same regardless of what you are doing. For example, this is how we'd define a model:

#[derive(Debug, toasty::Model)]
struct Article {
    #[key]
    #[auto]
    id: uuid::Uuid,

    #[unique]
    title: String,

    views: i64,
}

#Local database, concurrent writes

The easiest way to start with Turso and Toasty is a plain file on disk: no server, no network. The default behavior is the same as SQLite's: writes are serialized on a single lock. But with the concurrent_writes flag, we can achieve concurrent behavior:

let driver = toasty_driver_turso::Turso::file("local.db")
    .concurrent_writes();

let mut db = toasty::Db::builder()
    .models(toasty::models!(Article))
    .build(driver)
    .await?;
db.push_schema().await?;

To exercise it, seed one article per writer and spawn eight tasks. Db is cheaply clonable (clones share the connection pool), so each task gets its own handle.

To achieve write concurrency in Turso, we need to wrap the transactions in BEGIN CONCURRENT. Toasty does that automatically when the database is opened in concurrent_writes() mode.

For example, the following code will update 20 articles concurrently to bump their view count:

let mut db = db.clone();
tokio::spawn(async move {
    for _ in 0..20 {
        let mut tx = db.transaction().await?; // BEGIN CONCURRENT

        let mut article = Article::filter_by_title(format!("Article {w}"))
            .get(&mut tx)
            .await?;
        toasty::update!(article { views: article.views + 1 })
            .exec(&mut tx)
            .await?;

        tx.commit().await?;
    }
    Ok::<(), toasty::Error>(())
});

#Serverless: SQLite over HTTP

In environments where you don't want to deal with a local file (edge functions, stateless compute), or just because you want the convenience of accessing a fully-remote database, you can use Turso with the serverless driver — the same model as our serverless JavaScript driver, now from Rust. The main difference is that instead of passing a file, you pass an URL and a token. There is also no need to specify the concurrency mode, since that will be determined by the remote.

let driver = toasty_driver_turso::Turso::new(url)?
    .with_auth_token(token);

Everything else is the same Toasty code as the local examples. Here we're creating an article, and then doing a transaction over the wire:

let article = toasty::create!(Article {
    title: format!(
        "Hello from a serverless connection ({})",
        uuid::Uuid::new_v4()
    ),
    views: 0,
})
.exec(&mut db)
.await?;

let mut tx = db.transaction().await?;
let mut fetched = Article::get_by_id(&mut tx, &article.id).await?;
toasty::update!(fetched { views: fetched.views + 1 })
    .exec(&mut tx)
    .await?;
tx.commit().await?;

#Prefer batches over the network

For remote databases, there is one thing to keep in mind: over HTTP, every statement in a transaction is an extra round trip to the server. That's the right tool when later statements depend on earlier results, but when they don't, batches may yield better performance: toasty::batch() composes independent statements into a single request, and the results come back as a typed tuple matching the inputs. Here we create two articles and list everything in one round trip:

let (first, second, articles) = toasty::batch((
    toasty::create!(Article {
        title: format!("Batched article one ({})", uuid::Uuid::new_v4()),
        views: 0,
    }),
    toasty::create!(Article {
        title: format!("Batched article two ({})", uuid::Uuid::new_v4()),
        views: 0,
    }),
    Article::all(),
))
.exec(&mut db)
.await?;

And because batches work against local files too, your code can always stay the same: write with batches where it makes sense, and the one-round-trip benefit kicks in automatically whenever the database happens to be on the other side of a network.

#Sync: local-first access, backed by a remote database

The last mode we'll explore is Sync. Because sync has both a local and a remote component, you have to specify both the local file, and the remote location to connect to:

let driver = toasty_driver_turso::Turso::file("synced.db")
    .with_remote_url(url)
    .with_auth_token(token);

The one main difference, is that Turso lets you control when to pull changes into the local files, or push changes to the remote.

You can do this inside a task periodically, or at specific points where you need to guarantee durability (for example once a user presses the "Save" button):

if driver.pull().await? {
    println!("pulled fresh changes from Turso Cloud");
}
driver.push().await?;

#Summary

Toasty is an async Rust ORM that brings the Rails "let's get it done" energy to Rust, and Turso is now fully supported across all three of its modes. The same Article model and the same queries ran unchanged against a plain local file with concurrent writes, a local-first replica that pushes and pulls from Turso Cloud, and a fully remote database over HTTP — the only thing that changed between those examples was the driver constructor.

That's the promise: start with a file on disk and zero setup, and when your app grows into sync or serverless, your data layer comes along without a rewrite.

Go try it! The Toasty guide will get you from zero to a running app, and you can create a free Turso database when you're ready to go to the cloud. The code is on GitHub at tokio-rs/toasty; issues, ideas, and contributions are very welcome. We'd love to hear what you build.