Turso
Turso is an edge database built on libSQL (open-source SQLite fork). Use file: locally and a remote URL + auth token in production.
Why Turso with Resuma
- SQLite semantics — zero server to manage for small/medium apps
- Edge replicas — data close to Fly regions (Paris, Ashburn, etc.)
- Identical SQL in dev and prod when you use libSQL file mode locally
- Pairs with SQLx (SQLite driver) or the official
libsqlcrate
Install
[dependencies]
libsql = "0.6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
# Optional: SQLx with SQLite for compile-time queries against Turso
# sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "macros"] }Client helper
// src/turso.rs
use libsql::{Builder, Connection};
pub async fn connect() -> anyhow::Result<Connection> {
let url = std::env::var("TURSO_DATABASE_URL")
.unwrap_or_else(|_| "file:local.db".into());
let db = if url.starts_with("file:") {
Builder::new_local(url.strip_prefix("file:").unwrap()).build().await?
} else {
let token = std::env::var("TURSO_AUTH_TOKEN")?;
Builder::new_remote(url, token).build().await?
};
Ok(db.connect()?)
}Local file database (dev / CI)
# Create schema
sqlite3 local.db
# sqlite> CREATE TABLE todo (id INTEGER PRIMARY KEY, task TEXT NOT NULL, done INTEGER DEFAULT 0);
# sqlite> .quit
# .env (never commit)
TURSO_DATABASE_URL=file:local.db
# No auth token needed for file: URLsListing todos - #[load]
#[derive(Clone, Serialize, Deserialize)]
struct Todo {
id: i64,
task: String,
done: bool,
}
#[load]
async fn todos(_req: &FlowRequest) -> Vec<Todo> {
let conn = crate::turso::connect().await.ok()?;
let mut rows = conn
.query("SELECT id, task, done FROM todo ORDER BY id", ())
.await
.ok()?;
let mut out = Vec::new();
while let Ok(Some(row)) = rows.next().await {
out.push(Todo {
id: row.get::<i64>(0).unwrap_or(0),
task: row.get::<String>(1).unwrap_or_default(),
done: row.get::<i64>(2).unwrap_or(0) != 0,
});
}
out
}
pub fn page(_req: FlowRequest) -> View {
let items = use_todos_load();
view! {
<ul>
{items.iter().map(|t| view! {
<li key={t.id.to_string()}>{t.task.clone()}</li>
}).collect::<Vec<_>>()}
</ul>
}
}Adding a todo - #[submit]
#[derive(Deserialize)]
struct NewTodo {
task: String,
}
#[submit]
async fn add_todo(form: NewTodo, _req: &FlowRequest) -> Result<(), SubmitError> {
if form.task.trim().is_empty() {
return Err(SubmitError::new("Fix errors").field("task", "Required"));
}
let conn = crate::turso::connect()
.await
.map_err(|_| SubmitError::new("Database unavailable"))?;
conn.execute("INSERT INTO todo (task) VALUES (?)", [form.task.as_str()])
.await
.map_err(|_| SubmitError::new("Insert failed"))?;
Ok(())
}Production Turso database
# Turso CLI
turso db create my-app
turso db show my-app --url
turso db tokens create my-app
# Fly secrets
fly secrets set \
TURSO_DATABASE_URL="libsql://my-app-....turso.io" \
TURSO_AUTH_TOKEN="eyJ..." \
--app my-appDeploy on Fly.io
Turso complements Fly's global edge: your Resuma app on Fly + Turso replica in the same region keeps TTFB low for data-heavy loaders. No Postgres VM required.
For relational workloads that outgrow SQLite, migrate to SQLx + PostgreSQL — the Flow loader/submit code shape stays the same.
SQLx vs libsql client
| Approach | Best for |
|---|---|
libsql crate | Turso-native features, embedded replicas, simplest Turso docs parity |
sqlx + SQLite | Compile-time query macros, shared pool code with Postgres builds |