SQLx
SQLx is an async, SQL-native, type-safe database layer for Rust. Use it inside #[load], #[submit], and #[server] — all server-only.
Why SQLx for Resuma
- Async-first — matches Tokio + Axum under the hood
- Compile-time query checking with
sqlx::query!/sqlx::query_as! - Works with PostgreSQL, SQLite, MySQL — same Flow patterns everywhere
- Migrations via
sqlx migrate
Install
# Cargo.toml — pick one driver feature
[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "macros", "migrate"] }
# or for Turso/local SQLite:
# sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "macros", "migrate"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }Connection pool (app startup)
// src/db.rs
use sqlx::postgres::PgPoolOptions;
use std::sync::OnceLock;
static POOL: OnceLock<sqlx::PgPool> = OnceLock::new();
pub async fn init_db() -> anyhow::Result<()> {
let url = std::env::var("DATABASE_URL")?;
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&url)
.await?;
POOL.set(pool).map_err(|_| anyhow::anyhow!("pool already set"))?;
Ok(())
}
pub fn pool() -> &'static sqlx::PgPool {
POOL.get().expect("call init_db() before serve()")
}Listing users with #[load]
#[derive(Clone, Serialize, Deserialize, sqlx::FromRow)]
struct User {
id: i64,
name: String,
email: String,
}
#[load]
async fn users(_req: &FlowRequest) -> Vec<User> {
sqlx::query_as!(User, "SELECT id, name, email FROM users ORDER BY id")
.fetch_all(crate::db::pool())
.await
.unwrap_or_default()
}
// pages/users/index.rs
pub fn page(_req: FlowRequest) -> View {
let users = use_users_load();
view! {
<section>
<h1>"User directory"</h1>
<ul>
{users.iter().map(|u| view! {
<li key={u.id.to_string()}>
<a href={format!("/users/{}", u.id)}>
{format!("{} ({})", u.name, u.email)}
</a>
</li>
}).collect::<Vec<_>>()}
</ul>
</section>
}
}User detail - param from FlowRequest
#[load]
async fn user_detail(req: &FlowRequest) -> Option<User> {
let id: i64 = req.param("id")?.parse().ok()?;
sqlx::query_as!(User, "SELECT id, name, email FROM users WHERE id = $1", id)
.fetch_optional(crate::db::pool())
.await
.ok()
.flatten()
}Adding a user with #[submit]
#[derive(Deserialize)]
struct CreateUserForm {
name: String,
email: String,
}
#[submit]
async fn create_user(form: CreateUserForm, _req: &FlowRequest)
-> Result<(), SubmitError>
{
if form.email.is_empty() {
return Err(SubmitError::new("Fix errors").field("email", "Required"));
}
sqlx::query!(
"INSERT INTO users (name, email) VALUES ($1, $2)",
form.name,
form.email,
)
.execute(crate::db::pool())
.await
.map_err(|_| SubmitError::new("Could not create user"))?;
Ok(())
}
// In the page:
view! {
<Form submit={create_user}>
<label>"Name" <input name="name" type="text" /></label>
<label>"Email" <input name="email" type="email" /></label>
<button type="submit">"Create"</button>
</Form>
}Server RPC - #[server]
#[server]
async fn toggle_todo(id: i64, done: bool) -> Result<bool, String> {
sqlx::query!("UPDATE todos SET done = $1 WHERE id = $2", done, id)
.execute(crate::db::pool())
.await
.map_err(|e| e.to_string())?;
Ok(done)
}Migrations
# migrations/001_users.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
# CLI
sqlx migrate run
# offline check in CI: cargo sqlx prepare --checkGlobal extensions
db::init_db().await?;
FlowApp::new()
.with_extension("db", "ready")
.auto_pages(pages_root, PagesRegistry)
.serve(FlowServeOptions::default())
.await
// In #[load]:
async fn users(req: &FlowRequest) -> Vec<User> {
assert_eq!(req.extension("db").and_then(|v| v.as_str()), Some("ready"));
// ...
}Deploy on Fly.io
Attach Fly Postgres and set DATABASE_URL as a secret. Call init_db().await before FlowApp::serve(). See Docker deploy.
For edge SQLite instead of Postgres, use Turso + libSQL.