Rust SDK

Rust Quick Start

WoWSQL Rust SDK — Get started in minutes

Get your API keys: In your WoWSQL project dashboard, go to Settings → API to find your anon key (wowsql_anon_...) and service role key (wowsql_service_...). The anon key is safe for client-side use (respects Row Level Security). The service role key bypasses RLS and should only be used server-side.

Installation

Official package: View on crates.io →

cargo add wowsql
cargo add tokio --features full
cargo add serde_json

Or add to Cargo.toml:

[dependencies]
wowsql = "3.10"
tokio = { version = "1.0", features = ["full"] }

Initialize Client

The SDK talks to PostgREST at /rest/v1/. Your project URL resolves to https://myproject.wowsqlconnect.com.

use serde_json::Value;
use wowsql::WOWSQLClient;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize client with your anon key (respects Row Level Security)
    let client = WOWSQLClient::new(
        "https://myproject.wowsqlconnect.com",  // resolves to /rest/v1/
        "wowsql_anon_your_key_here",            // anon key from Settings → API
    )?;

    // Always use select() before get()/execute()
    let users = client.table("users").select(&["*"]).limit(10).execute::<Value>().await?;
    for user in users.data {
        println!("{}", user["name"]);
    }
    Ok(())
}

Query Data

Basic Queries

use serde_json::{json, Value};

let users = client.table("users").select(&["*"]).get::<Value>().await?;
let adults = client.table("users").select(&["*"]).gte("age", json!(18)).get::<Value>().await?;
let names = client.table("users").select(&["id", "name"]).get::<Value>().await?;

Advanced Queries

use wowsql::models::SortDirection;

let active_users = client.table("users")
    .select(&["*"])
    .eq("status", json!("active"))
    .gt("age", json!(18))
    .order("created_at", SortDirection::Desc)
    .limit(20)
    .offset(0)
    .execute::<Value>()
    .await?;
println!("Found {} users", active_users.count);

Filter Helper Methods

let users = client.table("users")
    .eq("status", json!("active"))
    .gt("age", json!(18))
    .like("email", "%@gmail.com")
    .in_("role", vec![json!("admin"), json!("moderator")])
    .between("created_at", json!("2024-01-01"), json!("2024-12-31"))
    .get::<Value>()
    .await?;

GROUP BY and Aggregates

Basic GROUP BY

let result = client.table("products")
    .select(&["category", "COUNT(*) as count", "AVG(price) as avg_price"])
    .group_by(&["category"])
    .get::<Value>()
    .await?;

HAVING Clause

let result = client.table("products")
    .select(&["category", "COUNT(*) as count"])
    .group_by(&["category"])
    .having("COUNT(*)", "gt", json!(10))
    .get::<Value>()
    .await?;

CRUD Operations

Create Record

let result = client.table("users").create(json!({
    "name": "John Doe",
    "email": "john@example.com",
    "age": 30
})).await?;
println!("Created user with ID: {}", result.id);

Update Record

client.table("users").update(json!(123), json!({
    "email": "newemail@example.com"
})).await?;

Delete Record

client.table("users").delete(json!(123)).await?;

Get Single Record by ID

let user = client.table("users").get_by_id(json!(123)).await?;
println!("User: {}", user["name"]);

Get First Record

let user = client.table("users").eq("status", json!("active")).first::<Value>().await?;
if let Some(u) = user { println!("First active user: {}", u["name"]); }

Filter Operators

Available operators: eq, neq, gt, gte, lt, lte, like, is, in, not_in, between, not_between. Use .eq("col", value), .filter("col", "op", value), etc.

Error Handling

match client.table("users").select(&["*"]).get::<Value>().await {
    Ok(users) => println!("Success: {} users", users.data.len()),
    Err(e) => {
        if e.is_authentication() { println!("Authentication failed — check your API key"); }
        else if e.is_not_found() { println!("Table or resource not found"); }
        else { println!("Error: {}", e); }
    }
}

Authentication

Auth endpoints are served at /auth/v1/. Use your anon key as the apikey header.

Sign Up & Sign In

use wowsql::AuthClient;

let auth = AuthClient::new(
    "myproject",
    "wowsql_anon_your_key_here",    // anon key — POST /auth/v1/signup, POST /auth/v1/login
)?;

// Sign up (POST /auth/v1/signup): sign_up(email, password, full_name, user_metadata)
let result = auth.sign_up("user@example.com", "SecurePassword123", Some("John Doe"), None).await?;

// Sign in (POST /auth/v1/login): returns a session, or a 2FA challenge
let login = auth.sign_in("user@example.com", "SecurePassword123").await?;
if login.requires_2fa {
    // TOTP is on for this user: finish with verify_two_factor_login (see Two-Factor Authentication)
    println!("2FA required, method: {:?}", login.method);
} else if let Some(session) = &login.session {
    println!("Access token: {}", session.access_token);
}

CAPTCHA (Turnstile)

Optional. Only required when dashboard Attack Protection → Turnstile is enabled. Create a Cloudflare Turnstile widget for your app domain and paste the site key and secret in Attack Protection (WoWSQL does not share a platform widget). GET /auth/v1/settings returns your captcha.site_key. Render Turnstile with that key, then pass the token. Existing sign_in / sign_up signatures are unchanged — use the *_with_captcha methods.

auth.sign_up_with_captcha("user@example.com", "SecurePassword123", None, None, Some(&turnstile_token)).await?;
auth.sign_in_with_captcha("user@example.com", "SecurePassword123", Some(&turnstile_token)).await?;
auth.forgot_password_with_captcha("user@example.com", Some(&turnstile_token)).await?;
auth.send_otp_with_captcha("user@example.com", "login", Some(&turnstile_token)).await?;
auth.send_magic_link_with_captcha("user@example.com", "login", Some(&turnstile_token)).await?;
auth.resend_verification_with_captcha("user@example.com", Some(&turnstile_token)).await?;

Password Reset

auth.forgot_password("user@example.com").await?;
auth.reset_password("reset_token", "NewSecurePassword123").await?;

OTP Authentication

Email: send_otp / verify_otp. Phone SMS: send_otp_phone / verify_otp_phone. Phone OTP requires Auth → SMS / Phone OTP (Twilio, Fast2SMS, or MSG91).

// Email OTP
auth.send_otp("user@example.com", "login").await?;
let email_login = auth.verify_otp("user@example.com", "123456", "login", None).await?;

// Phone OTP (SMS)
auth.send_otp_phone("+15551234567", "login").await?;
let phone_login = auth.verify_otp_phone("+15551234567", "123456", "login", None).await?;
println!("Logged in: {:?}", phone_login.user);
auth.send_magic_link("user@example.com", "login").await?;

OAuth Authentication

Setup checklist — do this once per provider, per project:
1. Dashboard → Project → Auth → Providers: enable the provider, paste Client ID and Client Secret (no leading/trailing spaces).
2. Copy the Redirect URI shown: https://<slug>.wowsqlconnect.com/auth/v1/oauth/<provider>/callback and register it in your provider's developer console.
3. The callback URL is handled entirely by WoWSQL — your app does not send an API key on it. WoWSQL exchanges the code and redirects to your frontend_redirect_uri with ?access_token=&refresh_token=.

// ── Step 1: Server-side — get the authorization URL ─────────────────────────
// Returns HashMap<String, Value> with authorization_url, provider, backend_callback_url, frontend_redirect_uri
let oauth = auth.get_oauth_authorization_url(
    "google",
    Some("https://yourapp.com/auth/callback"),
).await?;
// Redirect the browser to oauth["authorization_url"]
// Google calls: https://<slug>.wowsqlconnect.com/auth/v1/oauth/google/callback
// WoWSQL exchanges the code and redirects to your frontend_redirect_uri.


// ── Step 2: Frontend — read tokens from the redirect ────────────────────────
// WoWSQL redirects to: https://yourapp.com/auth/callback?access_token=...&refresh_token=...
// Parse them from the URL in your frontend.


// ── (Advanced) Manual code exchange — only if you handle the callback yourself
let result = auth.exchange_oauth_callback(
    "google",
    "authorization_code_from_callback",
    Some("https://yourapp.com/auth/callback"),  // optional; the server exchanges the code with the project callback URL
).await?;
if let Some(user) = result.user { println!("OAuth login successful: {}", user.id); }

Session and Token Refresh

Token refresh calls POST /auth/v1/refresh-token with the stored refresh token and stores the new token pair. From 3.10.0 every SDK uses this route; before 3.10.0 the TypeScript and Java SDKs called routes that do not exist, and Flutter and Rust had no refresh method.

// Current user → GET /auth/v1/me (None = use the stored access token)
let me = auth.get_user(None).await?;

// Refresh tokens → POST /auth/v1/refresh-token (None = use the stored refresh token)
let refreshed = auth.refresh_token(None).await?;

// Read or restore the stored session
let session = auth.get_session();
auth.set_session("saved_access_token", "saved_refresh_token");

// Sign out → POST /auth/v1/logout
auth.logout(None).await?;

Two-Factor Authentication (TOTP)

Added in 3.10.0. When a user has TOTP two-factor authentication enabled, sign-in does not return a session. It returns a challenge instead: requires_2fa is true and a short-lived pending_token is set (SDK field names are in the example). Finish the sign-in with the verify call, using a 6-digit code from the authenticator app or one of the user's backup codes. Status, setup, enable, disable and recovery-code calls run as the signed-in user: the SDK sends your API key and the stored access token. Enabling 2FA and rotating recovery codes return new backup codes once; show them to the user and do not store them in plain text.

Not wrapped in this SDK in 3.10.0: disabling 2FA (POST /auth/v1/2fa/disable) and rotating recovery codes (POST /auth/v1/2fa/recovery-codes). Call the REST endpoint directly with the apikey header and Authorization: Bearer <access token>; the body is {"code": "123456"} (disable also accepts {"backup_code": "..."}). Rust 3.10.0 also does not wrap /settings, anonymous sign-in, identity linking or email change.

// 1. Enrol (user is signed in; None = use the stored access token)
let setup = auth.setup_totp(None).await?;             // POST /auth/v1/2fa/setup-totp
// Show setup["qr_code_url"] (a PNG data URI when available) or setup["otpauth_uri"]
let enabled = auth.enable_totp("123456", None).await?; // POST /auth/v1/2fa/verify-and-enable
println!("{:?}", enabled.get("backup_codes"));          // shown once

// 2. Sign in when 2FA is on
let login = auth.sign_in("user@example.com", "SecurePassword123").await?;
if login.requires_2fa {
    let pending = login.pending_token.clone().unwrap_or_default();
    // verify_two_factor_login(pending_token, code, backup_code) → POST /auth/v1/2fa/verify-login
    let done = auth.verify_two_factor_login(&pending, Some("123456"), None).await?;
}

// 3. Status
let status = auth.get_two_factor_status(None).await?; // enabled, method

Storage Operations

Initialize Storage

Storage endpoints are served at /storage/v1/. The SDK storage client itself calls /api/v1/storage/projects/{project_slug}/… on your project host (buckets, files, stats).

use wowsql::StorageClient;

let storage = StorageClient::new(
    "myproject",
    "wowsql_anon_your_key_here",    // anon key
)?;

File Operations

// Upload a local file: upload_from_path(file_path, bucket, folder)
// The third argument is the folder (sent as ?folder=); the file keeps its own name.
let result = storage.upload_from_path("path/to/document.pdf", "documents", Some("uploads")).await?;   // → uploads/document.pdf

// Or upload bytes: upload(bucket, data, folder, file_name). Pass file_name, or the object is named "file".
let file_data = std::fs::read("path/to/document.pdf")?;
storage.upload("documents", &file_data, Some("uploads"), Some("document.pdf")).await?;

// Public URL (public buckets)
let url = storage.get_public_url("documents", "uploads/document.pdf");

// Download bytes
let bytes = storage.download("documents", "uploads/document.pdf").await?;

// List files: list_files(bucket, options) — options is an optional HashMap (prefix, limit, offset)
let files = storage.list_files("documents", None).await?;

// Delete file
storage.delete_file("documents", "uploads/document.pdf").await?;

// Check usage (GET .../stats). get_quota() calls .../quota, which returns the same fields as /stats.
let stats = storage.get_stats().await?;
println!("{} files, {:.2} GB used", stats.total_files, stats.total_size_gb);

Download or View File

Via REST: use GET /storage/v1/object/{bucket}/{path} for download, or GET /storage/v1/object/public/{bucket}/{path} for public access.

Realtime

The Rust crate does not yet ship a realtime client. Connect with any WebSocket library using the same anon or service_role key as REST.

WebSocket URL: wss://<project>.wowsqlconnect.com/realtime/v1/websocket?apikey=<wowsql_anon_... or wowsql_service_...>. Enable a table first with POST /realtime/v1/enable and body {"schema_name":"public","table_name":"messages"}. Channel broadcast / presence do not need a Postgres trigger. Payloads are capped at 64 KiB. Missing key closes 4001; invalid key closes 4003 (do not reconnect). Multi-replica deployments need REDIS_URL. Official clients exist in TypeScript, Python, Go, Java, Kotlin, C#, PHP, Ruby, Swift, and Flutter.

{
  "type": "subscribe",
  "schema": "public",
  "table": "messages",
  "event": "*"
}
{"type":"join","channel":"chat"}
{"type":"broadcast","channel":"chat","event":"typing","payload":{"user":"alice"}}
{"type":"presence","event":"track","channel":"chat","payload":{"user":"alice"}}

Schema Management (Creating Tables)

Requires service role key. POST /api/v2/schema/tables requires the primary key column to be PostgreSQL UUID. Use ColumnDefinition::new("id", "UUID").auto_increment(true) for DEFAULT gen_random_uuid(). Raw CREATE TABLE via execute_sql must not use SERIAL PRIMARY KEY (or similar integer PK).

Use SchemaClient with a service role key:

use wowsql::{ColumnDefinition, SchemaClient};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let schema = SchemaClient::new(
        "https://your-project.wowsqlconnect.com",
        "wowsql_service_your_key_here",          // service role key — bypasses RLS
    )?;

    schema
        .create_table(
            "products",
            vec![
                ColumnDefinition::new("id", "UUID").auto_increment(true),
                ColumnDefinition::new("name", "VARCHAR(255)").not_null(true),
                ColumnDefinition::new("price", "DECIMAL(10,2)").not_null(true),
            ],
            "id",
            None,
        )
        .await?;

    Ok(())
}