| Crates.io | supabase-lib-rs |
| lib.rs | supabase-lib-rs |
| version | 0.5.0 |
| created_at | 2025-08-13 15:36:57.458933+00 |
| updated_at | 2025-08-15 15:39:52.141878+00 |
| description | A comprehensive, production-ready Rust client library for Supabase with full cross-platform support (native + WASM) |
| homepage | https://github.com/nizovtsevnv/supabase-lib-rs |
| repository | https://github.com/nizovtsevnv/supabase-lib-rs |
| max_upload_size | |
| id | 1793750 |
| size | 714,473 |
A comprehensive, production-ready Rust client library for Supabase. This library provides a clean, type-safe, and efficient interface to interact with all Supabase services.
Add this to your Cargo.toml:
[dependencies]
supabase-lib-rs = "0.4.2"
tokio = { version = "1.0", features = ["full"] }
Or use cargo to add it:
cargo add supabase-lib-rs
use supabase::prelude::*;
#[tokio::main]
async fn main() -> Result<()> {
// Initialize the client
let client = Client::new(
"https://your-project.supabase.co",
"your-anon-key"
)?;
// Or for admin operations with service role key
let admin_client = Client::new_with_service_role(
"https://your-project.supabase.co",
"your-anon-key",
"your-service-role-key"
)?;
// Authenticate user
let auth_response = client
.auth()
.sign_in_with_email_and_password("user@example.com", "password")
.await?;
println!("User signed in: {:?}", auth_response.user);
// Query database
let posts = client
.database()
.from("posts")
.select("id, title, content")
.eq("published", "true")
.limit(10)
.execute::<Post>()
.await?;
println!("Found {} posts", posts.len());
// Upload file to storage
let upload_result = client
.storage()
.upload("avatars", "user-123.jpg", file_bytes, None)
.await?;
println!("File uploaded: {}", upload_result.key);
// Subscribe to realtime changes
let _subscription_id = client
.realtime()
.channel("posts")
.table("posts")
.subscribe(|message| {
println!("Realtime update: {:?}", message);
})
.await?;
Ok(())
}
use supabase::prelude::*;
let client = Client::new("your-url", "your-key")?;
// Sign up new user
let response = client
.auth()
.sign_up_with_email_and_password("user@example.com", "password123")
.await?;
// Sign in existing user
let response = client
.auth()
.sign_in_with_email_and_password("user@example.com", "password123")
.await?;
// Update user profile
let response = client
.auth()
.update_user(
Some("new@example.com".to_string()),
None,
Some(serde_json::json!({"display_name": "New Name"}))
)
.await?;
// Sign out
client.auth().sign_out().await?;
use supabase::prelude::*;
let client = Client::new("your-url", "your-key")?;
// Setup TOTP (Google Authenticator, Authy, etc.)
let totp_setup = client
.auth()
.setup_totp("My Authenticator App")
.await?;
println!("Scan this QR code with your authenticator app:");
println!("{}", totp_setup.qr_code);
println!("Or enter this secret manually: {}", totp_setup.secret);
// Setup SMS MFA with international phone number
let sms_factor = client
.auth()
.setup_sms_mfa("+1-555-123-4567", "My Phone", Some("US"))
.await?;
// Create MFA challenge
let challenge = client
.auth()
.create_mfa_challenge(totp_setup.factor_id)
.await?;
// Verify with TOTP code from authenticator app
let auth_response = client
.auth()
.verify_mfa_challenge(
totp_setup.factor_id,
challenge.id,
"123456" // Code from authenticator app
)
.await?;
// List configured MFA factors
let factors = client.auth().list_mfa_factors().await?;
for factor in factors {
println!("MFA Factor: {} ({})", factor.friendly_name, factor.factor_type);
}
// Check if token needs refresh with 5-minute buffer
if client.auth().needs_refresh_with_buffer(300)? {
// Refresh token with advanced error handling
match client.auth().refresh_token_advanced().await {
Ok(session) => println!("Token refreshed successfully!"),
Err(e) if e.is_retryable() => {
println!("Retryable error - wait {} seconds", e.retry_after().unwrap_or(60));
}
Err(e) => println!("Authentication required: {}", e),
}
}
// Get detailed token information
if let Some(metadata) = client.auth().get_token_metadata()? {
println!("Token expires at: {}", metadata.expires_at);
println!("Refresh count: {}", metadata.refresh_count);
}
// Validate token locally (no API call)
let is_valid = client.auth().validate_token_local()?;
println!("Token is valid: {}", is_valid);
use supabase::session::{SessionManager, SessionManagerConfig, storage::create_default_storage};
// Create session manager with default storage
let storage_backend = create_default_storage()?;
let config = SessionManagerConfig {
storage_backend,
enable_cross_tab_sync: true,
session_key_prefix: "myapp_".to_string(),
default_expiry_seconds: 3600, // 1 hour
enable_encryption: false,
encryption_key: None,
enable_monitoring: true,
max_memory_sessions: 100,
sync_interval_seconds: 30,
};
let session_manager = SessionManager::new(config);
session_manager.initialize().await?;
// Store session with cross-tab sync
let session_id = session_manager.store_session(session).await?;
println!("Session stored: {}", session_id);
// Set up session event listener
let listener_id = session_manager.on_session_event(|event| {
match event {
SessionEvent::Created { session_id } => {
println!("New session created: {}", session_id);
}
SessionEvent::Updated { session_id, changes } => {
println!("Session {} updated: {:?}", session_id, changes);
}
SessionEvent::Destroyed { session_id, reason } => {
println!("Session {} destroyed: {}", session_id, reason);
}
_ => {}
}
});
// List all active sessions
let sessions = session_manager.list_sessions().await?;
println!("Active sessions: {}", sessions.len());
// Session will automatically sync across browser tabs/windows
// and persist according to platform (localStorage, filesystem, etc.)
use supabase::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct Post {
id: Option<i32>,
title: String,
content: String,
published: bool,
}
let client = Client::new("your-url", "your-key")?;
// SELECT with filters and ordering
let posts = client
.database()
.from("posts")
.select("*")
.eq("published", "true")
.gt("created_at", "2023-01-01")
.order("created_at", OrderDirection::Descending)
.limit(20)
.execute::<Post>()
.await?;
// INSERT new record
let new_post = Post {
id: None,
title: "Hello Rust".to_string(),
content: "Content here".to_string(),
published: true,
};
let inserted = client
.database()
.insert("posts")
.values(new_post)?
.returning("*")
.execute::<Post>()
.await?;
// UPDATE records
let update_data = serde_json::json!({
"title": "Updated Title",
"updated_at": chrono::Utc::now()
});
let updated = client
.database()
.update("posts")
.set(update_data)?
.eq("id", "123")
.returning("*")
.execute::<Post>()
.await?;
// DELETE records
let deleted = client
.database()
.delete("posts")
.eq("published", "false")
.returning("id")
.execute::<Post>()
.await?;
// Call RPC function
let result = client
.database()
.rpc("search_posts", Some(serde_json::json!({
"search_term": "rust",
"limit": 10
})))
.await?;
use supabase::prelude::*;
use bytes::Bytes;
let client = Client::new("your-url", "your-key")?;
// Create bucket
let bucket = client
.storage()
.create_bucket("avatars", "User Avatars", true)
.await?;
// Upload file
let file_content = Bytes::from("Hello, World!");
let upload_result = client
.storage()
.upload("avatars", "user-123.txt", file_content, None)
.await?;
// Upload with options
let options = FileOptions {
content_type: Some("image/jpeg".to_string()),
cache_control: Some("max-age=3600".to_string()),
upsert: true,
};
let upload_result = client
.storage()
.upload("avatars", "avatar.jpg", image_bytes, Some(options))
.await?;
// Download file
let file_data = client
.storage()
.download("avatars", "user-123.txt")
.await?;
// Get public URL
let public_url = client
.storage()
.get_public_url("avatars", "avatar.jpg");
// Create signed URL
let signed_url = client
.storage()
.create_signed_url("private-files", "document.pdf", 3600)
.await?;
// List files
let files = client
.storage()
.list("avatars", Some("folder/"))
.await?;
use supabase::prelude::*;
let client = Client::new("your-url", "your-key")?;
let realtime = client.realtime();
// Connect to realtime
realtime.connect().await?;
// Subscribe to all changes on a table
let subscription_id = realtime
.channel("posts")
.table("posts")
.subscribe(|message| {
println!("Change detected: {}", message.event);
match message.event.as_str() {
"INSERT" => println!("New record: {:?}", message.payload.new),
"UPDATE" => {
println!("Old: {:?}", message.payload.old);
println!("New: {:?}", message.payload.new);
},
"DELETE" => println!("Deleted: {:?}", message.payload.old),
_ => {}
}
})
.await?;
// Subscribe to specific events
let insert_subscription = realtime
.channel("posts_inserts")
.table("posts")
.event(RealtimeEvent::Insert)
.subscribe(|message| {
println!("New post created!");
})
.await?;
// Subscribe with filters
let filtered_subscription = realtime
.channel("published_posts")
.table("posts")
.filter("published=eq.true")
.subscribe(|message| {
println!("Published post changed!");
})
.await?;
// Unsubscribe
realtime.unsubscribe(&subscription_id).await?;
This project uses Nix for reproducible development environments.
# Enter the development environment
nix develop
# Or run commands directly
nix develop -c cargo build
# Show all available commands
just --list
# Format code
just format
# Run linter
just lint
# Run tests
just test
# Build project
just build
# Run all checks (format, lint, test, build)
just check
# Start local Supabase for testing
just supabase-start
# Run examples
just example basic_usage
just example auth_example
just example database_example
just example storage_example
just example realtime_example
supabase-lib-rs/
โโโ src/
โ โโโ lib.rs # Library entry point
โ โโโ client.rs # Main Supabase client
โ โโโ auth.rs # Authentication module
โ โโโ database.rs # Database operations
โ โโโ storage.rs # File storage
โ โโโ realtime.rs # WebSocket subscriptions
โ โโโ error.rs # Error handling
โ โโโ types.rs # Common types and configurations
โโโ examples/ # Usage examples
โโโ tests/ # Integration tests
โโโ flake.nix # Nix development environment
โโโ justfile # Command runner configuration
โโโ CLAUDE.md # Development guidelines
This project has a comprehensive testing system with multiple levels of testing:
# Run unit tests only
just test-unit
# Run with documentation tests
just test
# Start local Supabase (requires Docker/Podman)
just supabase-start
# Run integration tests
just test-integration
# Run all tests (unit + doc + integration)
just test-all
The project includes a complete local Supabase setup using Docker Compose:
# Start all Supabase services
just supabase-start
# Check status
just supabase-status
# View logs
just supabase-logs [service]
# Stop services
just supabase-stop
# Clean up data
just supabase-clean
Services provided:
All integration tests automatically skip if Supabase is not available, making them safe for CI/CD.
While this library provides comprehensive Supabase functionality, some advanced features are planned for future releases:
onAuthStateChange event listeners (planned for v0.3.1)and(), or(), not() query logic (Added in v0.3.0)inner_join(), left_join() with aliases (Added in v0.3.0)upsert(), bulk_insert(), bulk_upsert() (Added in v0.3.0)raw_sql(), prepared_statement(), count_query() (Added in v0.3.0)textSearch() and advanced search operators (planned for v0.4.0)explain() and CSV export functionality (planned for v0.4.0)functions.invoke() for serverless functionsMost limitations can be worked around:
// Instead of OAuth, use magic links or email/password
let auth_response = client.auth()
.sign_up_with_email_and_password("user@example.com", "password")
.await?;
// Instead of logical operators, use multiple queries or raw SQL
let result = client.database()
.rpc("custom_query", Some(json!({"param": "value"})))
.await?;
// Instead of Edge Functions, use database RPC functions
let function_result = client.database()
.rpc("my_custom_function", Some(params))
.await?;
The library currently provides ~95% of core Supabase functionality and covers all common use cases for production applications.
v0.3.0 brings powerful database enhancements that make complex queries simple:
// โจ Logical operators for complex filtering
let active_adults = client.database()
.from("users")
.select("*")
.and(|q| {
q.gte("age", "18")
.eq("status", "active")
.not(|inner| inner.eq("banned", "true"))
})
.execute()
.await?;
// ๐ Query joins with related data
let posts_with_authors = client.database()
.from("posts")
.select("title,content")
.inner_join_as("authors", "name,email", "author")
.left_join("categories", "name")
.execute()
.await?;
// ๐ฆ Bulk operations for efficiency
let users = client.database()
.bulk_upsert("users", vec![
json!({"id": 1, "name": "Alice", "email": "alice@example.com"}),
json!({"id": 2, "name": "Bob", "email": "bob@example.com"}),
])
.await?;
// โก Database transactions
let result = client.database()
.begin_transaction()
.insert("users", json!({"name": "Charlie"}))
.update("profiles", json!({"bio": "Updated"}), "user_id = 1")
.delete("temp_data", "expired = true")
.commit()
.await?;
The examples/ directory contains comprehensive examples:
basic_usage.rs - Overview of all featuresauth_example.rs - Authentication flowsdatabase_example.rs - Database operationsstorage_example.rs - File storage operationsrealtime_example.rs - WebSocket subscriptionsRun examples with:
cargo run --example basic_usage
This library provides full WebAssembly support for web applications! You can use it with frameworks like Dioxus, Yew, or any WASM-based Rust web framework.
# Add WASM target
rustup target add wasm32-unknown-unknown
# Build for WASM
cargo build --target wasm32-unknown-unknown
# Build example for WASM
cargo build --target wasm32-unknown-unknown --example wasm_example
use supabase::{Client, Result};
use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub async fn main() {
let client = Client::new("your-url", "your-key").unwrap();
// Works exactly the same as native!
let todos = client
.database()
.from("todos")
.select("*")
.execute::<Todo>()
.await
.unwrap();
web_sys::console::log_1(&format!("Got {} todos", todos.len()).into());
}
Dioxus:
use dioxus::prelude::*;
use supabase::Client;
fn App(cx: Scope) -> Element {
let client = use_state(cx, || {
Client::new("your-url", "your-key").unwrap()
});
// Use client in your components...
}
Yew:
use yew::prelude::*;
use supabase::Client;
#[function_component(App)]
fn app() -> Html {
let client = use_state(|| {
Client::new("your-url", "your-key").unwrap()
});
// Use client in your components...
}
The library can be configured using environment variables. Copy .env.example to .env and fill in your actual values:
cp .env.example .env
Required variables:
SUPABASE_URL - Your Supabase project URLSUPABASE_ANON_KEY - Your Supabase anonymous keyOptional variables:
SUPABASE_SERVICE_ROLE_KEY - Service role key for admin operationsRUST_LOG - Log level (debug, info, warn, error)RUST_BACKTRACE - Enable backtrace (0, 1, full)SUPABASE_URLSUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEY (keep this secret!)use supabase::{Client, types::*};
let config = SupabaseConfig {
url: "https://your-project.supabase.co".to_string(),
key: "your-anon-key".to_string(),
service_role_key: None,
http_config: HttpConfig {
timeout: 30,
connect_timeout: 10,
max_redirects: 5,
default_headers: HashMap::new(),
},
auth_config: AuthConfig {
auto_refresh_token: true,
refresh_threshold: 300,
persist_session: true,
storage_key: "supabase.auth.token".to_string(),
},
database_config: DatabaseConfig {
schema: "public".to_string(),
max_retries: 3,
retry_delay: 1000,
},
storage_config: StorageConfig {
default_bucket: Some("uploads".to_string()),
upload_timeout: 300,
max_file_size: 50 * 1024 * 1024,
},
};
let client = Client::new_with_config(config)?;
We welcome contributions! Please see our Contributing Guidelines for details.
just checkjust checkThis project is licensed under the MIT License - see the LICENSE file for details.
Made with โค๏ธ for the Rust and Supabase communities