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 --check

Global 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.