supabase_rs 0.7.0

Lightweight Rust client for Supabase REST and GraphQL
Documentation

supabase_rs

current version: 0.7.0

An unofficial, lightweight Rust SDK for interacting with the Supabase REST and GraphQL APIs. This SDK provides a clean, chainable query-builder interface with comprehensive CRUD operations, advanced filtering capabilities, and optional modules for Storage and Realtime functionality.

Key Features

  • Pure REST API by default with optional nightly GraphQL support
  • Fluent Query Builder for intuitive filtering, ordering, limiting, and text search
  • Joins & Nested Selects for embedding related resources (left/inner join, FK disambiguation)
  • Complete CRUD Operations with Insert, Update, Upsert, and Delete helpers
  • Type-Safe Operations with Rust's strong type system
  • Connection Pooling built-in with reqwest::Client
  • Feature-Flagged Modules for Storage and Realtime (opt-in)
  • Comprehensive Error Handling with detailed error types
  • Async/Await Support throughout the entire API
  • Clone-Friendly Client for multi-threaded applications

Table of Contents

Installation

Add the crate to your project using Cargo:

[dependencies]
supabase_rs = "0.4.14"

# With optional features
supabase_rs = { version = "0.4.14", features = ["storage", "rustls"] }

Feature Combinations

# Basic REST API only (default)
supabase_rs = "0.4.14"

# With Storage support
supabase_rs = { version = "0.4.14", features = ["storage"] }

# With rustls instead of OpenSSL (recommended for cross-platform)
supabase_rs = { version = "0.4.14", features = ["rustls"] }

# With experimental GraphQL support (nightly)
supabase_rs = { version = "0.4.14", features = ["nightly"] }

# All features enabled
supabase_rs = { version = "0.4.14", features = ["storage", "rustls", "nightly"] }

Environment Setup

Create a .env file in your project root:

SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-anon-or-service-role-key

# Optional: Disable nightly warning messages
SUPABASE_RS_NO_NIGHTLY_MSG=true

# Optional: Use alternative endpoint format
SUPABASE_RS_DONT_REST_V1_URL=false

Tip: Use your service role key for server-side applications and anon key for client-side applications with Row Level Security (RLS) enabled.

Features and Flags

Core Features

Feature Description Stability Use Case
Default REST API operations with native TLS ✅ Stable Production applications
storage File upload/download operations ✅ Stable Applications with file management
rustls Use rustls instead of OpenSSL ✅ Stable Cross-platform deployments, Alpine Linux
nightly Experimental GraphQL support ⚠️ Experimental Advanced querying, development

Feature Flag Details

  • storage: Enables the Storage module for file operations with Supabase Storage buckets
  • rustls: Replaces OpenSSL with rustls for TLS connections (recommended for Docker/Alpine)
  • nightly: Unlocks GraphQL query capabilities (experimental, may have breaking changes)

Nightly Feature Configuration

The nightly feature shows warning messages by default. To disable them:

SUPABASE_RS_NO_NIGHTLY_MSG=true

⚠️ Warning: Nightly features are experimental and may introduce breaking changes without notice. Use with caution in production environments.

Quickstart

Basic Client Setup

use supabase_rs::SupabaseClient;
use dotenv::dotenv;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load environment variables from .env file
    dotenv().ok();
    
    // Initialize the Supabase client
    let client = SupabaseClient::new(
        std::env::var("SUPABASE_URL")?,
        std::env::var("SUPABASE_KEY")?,
    )?;
    
    // The client is ready to use!
    println!("✅ Supabase client initialized successfully");
    
    Ok(())
}

Helper Function for Reusable Client

use supabase_rs::SupabaseClient;

/// Creates a configured Supabase client instance
/// 
/// # Panics
/// Panics if SUPABASE_URL or SUPABASE_KEY environment variables are not set
fn create_client() -> SupabaseClient {
    SupabaseClient::new(
        std::env::var("SUPABASE_URL").expect("SUPABASE_URL must be set"),
        std::env::var("SUPABASE_KEY").expect("SUPABASE_KEY must be set"),
    ).expect("Failed to create Supabase client")
}

Multi-threaded Usage

use supabase_rs::SupabaseClient;
use std::sync::Arc;
use tokio::task;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Arc::new(create_client());
    
    // Clone is cheap - shares the underlying connection pool
    let client_clone = Arc::clone(&client);
    
    let handle = task::spawn(async move {
        // Use client_clone in another task
        let _result = client_clone.select("users").execute().await;
    });
    
    handle.await?;
    Ok(())
}

Database Operations

Basic CRUD

Insert Operations

use serde_json::json;
use supabase_rs::SupabaseClient;

let client = create_client();

// Basic insert - returns the new row's ID
let id = client.insert("pets", json!({
    "name": "scooby",
    "breed": "great_dane",
    "age": 7
})).await?;

println!("Inserted pet with ID: {}", id);

Insert with Unique Constraint Checking

// Insert only if the record doesn't already exist
// Checks all provided fields for uniqueness
let id = client.insert_if_unique("users", json!({
    "email": "user@example.com",
    "username": "john_doe"
})).await?;

// Returns error if a user with this email OR username already exists

Bulk Insert Operations

use serde::Serialize;

#[derive(Serialize)]
struct Pet {
    name: String,
    breed: String,
    age: i32,
}

let pets = vec![
    Pet { name: "Buddy".to_string(), breed: "golden_retriever".to_string(), age: 3 },
    Pet { name: "Luna".to_string(), breed: "border_collie".to_string(), age: 2 },
];

// Insert multiple records in a single request
client.bulk_insert("pets", pets).await?;

Update Operations

// Update by ID (default)
client.update("pets", "123", json!({
    "name": "scooby-doo",
    "age": 8
})).await?;

// Update by custom column
client.update_with_column_name(
    "users",
    "email",           // Column to match on
    "user@example.com", // Value to match
    json!({ "last_login": "2024-01-15T10:30:00Z" })
).await?;

Upsert Operations

// Insert or update if exists
client.upsert("pets", "123", json!({
    "name": "scooby-doo",
    "breed": "great_dane"
})).await?;

// Upsert without predefined ID (uses Supabase's conflict resolution)
client.upsert_without_defined_key("settings", json!({
    "user_id": "456",
    "theme": "dark",
    "notifications": true
})).await?;

Delete Operations

// Delete by ID
client.delete("pets", "123").await?;

// Delete by custom column
client.delete_without_defined_key("sessions", "token", "abc123").await?;

Advanced Querying

Complex Filtering

use serde_json::Value;

let client = create_client();

// Multiple filters with chaining
let adult_pets: Vec<Value> = client
    .select("pets")
    .gte("age", "2")                    // Age >= 2
    .neq("breed", "unknown")            // Breed != "unknown"
    .text_search("description", "friendly") // Full-text search
    .limit(20)
    .order("created_at", false)         // Newest first
    .execute()
    .await?;

Column Selection and Pagination

// Select specific columns with pagination
let users: Vec<Value> = client
    .from("users")
    .columns(vec!["id", "email", "created_at"])
    .range(0, 49)                       // Get first 50 records (0-49 inclusive)
    .order("created_at", true)          // Oldest first
    .execute()
    .await?;

// Using offset-based pagination
let page_2: Vec<Value> = client
    .from("users")
    .columns(vec!["id", "email"])
    .limit(25)
    .offset(25)                         // Skip first 25 records
    .execute()
    .await?;

Joins & Nested Selects

Embed related resources in a single request using PostgREST's resource embedding:

use supabase_rs::query::JoinSpec;
use serde_json::Value;

// Left join (default): parent rows with nested related data; empty array when no match
let sections: Vec<Value> = client
    .from("orchestral_sections")
    .select_with_joins(&["id", "name"], &[JoinSpec::new("instruments", &["id", "name"])])
    .execute()
    .await?;

// Inner join: filter parent rows to those with matching related rows
let woodwinds_only: Vec<Value> = client
    .from("orchestral_sections")
    .select_with_joins(
        &["id", "name"],
        &[JoinSpec::new("instruments", &["id", "name"]).inner()],
    )
    .eq("instruments.name", "flute")
    .execute()
    .await?;

// Explicit FK: disambiguate when multiple foreign keys exist between tables
let orders: Vec<Value> = client
    .from("orders")
    .select_with_joins(
        &["id", "name"],
        &[
            JoinSpec::new("addresses", &["name"]).alias("billing").foreign_key("orders_billing_address_id_fkey"),
            JoinSpec::new("addresses", &["name"]).alias("shipping").foreign_key("orders_shipping_address_id_fkey"),
        ],
    )
    .execute()
    .await?;

Advanced Filter Operations

// IN operator for multiple values
let specific_breeds: Vec<Value> = client
    .select("pets")
    .in_("breed", &["golden_retriever", "labrador", "poodle"])
    .execute()
    .await?;

// Null checking
let pets_without_age: Vec<Value> = client
    .select("pets")
    .eq("age", "is.null")
    .execute()
    .await?;

Bulk Operations

Batch Processing

use futures::future::try_join_all;

// Process multiple operations concurrently
let client = create_client();
let operations = vec![
    client.select("users").limit(100).execute(),
    client.select("pets").limit(100).execute(),
    client.select("orders").limit(100).execute(),
];

let results = try_join_all(operations).await?;
println!("Fetched {} datasets", results.len());

Error Handling

Comprehensive Error Management

use serde_json::json;

match client.insert("users", json!({ "email": "test@example.com" })).await {
    Ok(id) => {
        println!("✅ User created with ID: {}", id);
    },
    Err(error) => {
        if error.contains("409") {
            println!("⚠️ User already exists with this email");
            // Handle duplicate entry
        } else if error.contains("401") {
            println!("🔐 Authentication failed - check your API key");
        } else if error.contains("403") {
            println!("🚫 Insufficient permissions for this operation");
        } else {
            println!("❌ Unexpected error: {}", error);
        }
    }
}

Retry Logic Example

use tokio::time::{sleep, Duration};

async fn insert_with_retry(
    client: &SupabaseClient,
    table: &str,
    data: serde_json::Value,
    max_retries: u32
) -> Result<String, String> {
    for attempt in 1..=max_retries {
        match client.insert(table, data.clone()).await {
            Ok(id) => return Ok(id),
            Err(err) if attempt < max_retries => {
                println!("Attempt {} failed: {}. Retrying...", attempt, err);
                sleep(Duration::from_millis(1000 * attempt as u64)).await;
            },
            Err(err) => return Err(format!("Failed after {} attempts: {}", max_retries, err)),
        }
    }
    unreachable!()
}

Count Operations

** Performance Note**: Count operations are expensive and can be slow on large tables. Use sparingly and consider caching results.

// Count all records (expensive)
let total_users = client
    .select("users")
    .count()
    .execute()
    .await?;

// Count with filters (more efficient)
let active_users = client
    .select("users")
    .eq("status", "active")
    .count()
    .execute()
    .await?;

Storage Operations

** Requirement**: Enable the storage feature in your Cargo.toml

The Storage module provides comprehensive file management capabilities for Supabase Storage buckets.

File Download Operations

use supabase_rs::storage::SupabaseStorage;

// Initialize storage client
let storage = SupabaseStorage {
    supabase_url: std::env::var("SUPABASE_URL").unwrap(),
    bucket_name: "avatars".to_string(),
    filename: "user-123-avatar.jpg".to_string(),
};

// Download file to memory
let file_bytes = storage.download().await?;
println!("Downloaded {} bytes", file_bytes.len());

// Download file directly to disk
storage.save("./downloads/avatar.jpg").await?;

Advanced Storage Patterns

// Batch download multiple files
let files = vec!["file1.jpg", "file2.png", "file3.pdf"];
let mut downloads = Vec::new();

for filename in files {
    let storage = SupabaseStorage {
        supabase_url: env::var("SUPABASE_URL").unwrap(),
        bucket_name: "documents".to_string(),
        filename: filename.to_string(),
    };
    downloads.push(storage.download());
}

let results = try_join_all(downloads).await?;

GraphQL Support

⚠️ Experimental: Enable the nightly feature for GraphQL support. This is experimental and not production-ready.

GraphQL and REST operations can be mixed using the same client instance.

Basic GraphQL Query

use supabase_rs::graphql::{Request, RootTypes};
use serde_json::json;

let client = create_client();

let graphql_request = Request::new(
    client,
    json!({
        "query": r#"
            {
                usersCollection(first: 10) {
                    edges {
                        node {
                            id
                            email
                            created_at
                        }
                    }
                    pageInfo {
                        hasNextPage
                        endCursor
                    }
                }
            }
        "#
    }),
    RootTypes::Query
);

let response = graphql_request.send().await?;
println!("GraphQL Response: {:#?}", response);

GraphQL with Variables

let query_with_variables = Request::new(
    client,
    json!({
        "query": r#"
            query GetUsersByAge($minAge: Int!) {
                usersCollection(filter: { age: { gte: $minAge } }) {
                    edges {
                        node {
                            id
                            email
                            age
                        }
                    }
                }
            }
        "#,
        "variables": {
            "minAge": 18
        }
    }),
    RootTypes::Query
);

Mixing REST and GraphQL

// Use REST for simple operations
let new_user_id = client.insert("users", json!({
    "email": "newuser@example.com",
    "age": 25
})).await?;

// Use GraphQL for complex relational queries
let user_with_posts = Request::new(
    client.clone(),
    json!({
        "query": format!(r#"
            {{
                usersCollection(filter: {{ id: {{ eq: {} }} }}) {{
                    edges {{
                        node {{
                            id
                            email
                            postsCollection {{
                                edges {{
                                    node {{
                                        title
                                        content
                                    }}
                                }}
                            }}
                        }}
                    }}
                }}
            }}
        "#, new_user_id)
    }),
    RootTypes::Query
).send().await?;

Performance & Best Practices

Client Management

// ✅ Good: Reuse client instances (they're cheap to clone)
let client = create_client();
let client_clone = client.clone(); // Shares connection pool

// ❌ Avoid: Creating new clients repeatedly
// let client1 = SupabaseClient::new(...)?; // Don't do this in loops

Query Optimization

// ✅ Good: Use specific column selection
let users = client
    .from("users")
    .columns(vec!["id", "email"])  // Only fetch needed columns
    .limit(100)                    // Always use reasonable limits
    .execute()
    .await?;

// ✅ Good: Use range for pagination (more efficient than offset)
let page = client
    .from("users")
    .range(0, 99)                  // Get 100 records
    .execute()
    .await?;

// ⚠️ Use sparingly: Count operations are expensive
let count = client.select("users").count().execute().await?;

Batch Operations

// ✅ Good: Use bulk_insert for multiple records
client.bulk_insert("logs", vec![
    json!({"level": "info", "message": "Started"}),
    json!({"level": "info", "message": "Processing"}),
]).await?;

// ❌ Avoid: Individual inserts in loops
// for item in items {
//     client.insert("table", item).await?; // Inefficient
// }

Connection Pool Configuration

// For high-throughput applications, consider custom reqwest client
use reqwest::ClientBuilder;
use std::time::Duration;

let http_client = ClientBuilder::new()
    .pool_max_idle_per_host(10)
    .timeout(Duration::from_secs(30))
    .build()?;

// Note: Custom client configuration requires modifying SupabaseClient::new()

Testing

This repository includes comprehensive test coverage with both integration and unit tests.

Test Categories

  • Integration Tests: Test against live Supabase instances
  • Unit Tests: Test individual components in isolation
  • Performance Tests: Benchmark query performance

Running Tests

# Run all tests (requires SUPABASE_URL and SUPABASE_KEY)
cargo test

# Run only unit tests (no network required)
cargo test unit_

# Run specific test module
cargo test select_

# Run tests with output
cargo test -- --nocapture

# Run tests in release mode (faster)
cargo test --release

Test Environment Setup

Create a .env.test file for testing:

SUPABASE_URL=https://your-test-project.supabase.co
SUPABASE_KEY=your-test-key
SUPABASE_RS_NO_NIGHTLY_MSG=true

Writing Custom Tests

use supabase_rs::SupabaseClient;
use serde_json::json;

#[tokio::test]
async fn test_user_operations() -> Result<(), String> {
    let client = SupabaseClient::new(
        std::env::var("SUPABASE_URL").unwrap(),
        std::env::var("SUPABASE_KEY").unwrap(),
    ).unwrap();
    
    // Test insert
    let user_id = client.insert("users", json!({
        "email": "test@example.com",
        "name": "Test User"
    })).await?;
    
    // Test select
    let users = client
        .select("users")
        .eq("id", &user_id)
        .execute()
        .await?;
    
    assert!(!users.is_empty());
    
    // Cleanup
    client.delete("users", &user_id).await?;
    
    Ok(())
}

Troubleshooting

Common Issues and Solutions

Authentication Errors

Error: 401 Unauthorized

Solutions:

  • Verify your SUPABASE_URL and SUPABASE_KEY are correct
  • Ensure you're using the right key type (anon vs service role)
  • Check if your API key has expired

Permission Errors

Error: 403 Forbidden

Solutions:

  • Review your Row Level Security (RLS) policies
  • Ensure your API key has sufficient permissions
  • Check if the table/operation requires service role key

Connection Issues

Error: Connection timeout / Network error

Solutions:

  • Check your internet connection
  • Verify the Supabase URL is accessible
  • Consider increasing timeout values
  • Check if you're behind a corporate firewall

Duplicate Entry Errors

Error 409: Duplicate entry

Solutions:

  • Use insert_if_unique() instead of insert()
  • Check your unique constraints
  • Handle duplicates gracefully in your application logic

Performance Issues

Slow Queries

Symptoms:

  • Queries taking longer than expected
  • High memory usage

Solutions:

// Use column selection to reduce data transfer
let users = client
    .from("users")
    .columns(vec!["id", "email"])  // Only fetch needed columns
    .limit(100)                    // Always limit results
    .execute()
    .await?;

// Use pagination instead of fetching all records
let page = client
    .from("large_table")
    .range(0, 999)                 // Get 1000 records at a time
    .execute()
    .await?;

Memory Usage

High memory consumption solutions:

  • Use streaming for large datasets
  • Implement pagination
  • Process data in batches
  • Use specific column selection

Debugging

Enable Debug Logging

// Add to your Cargo.toml
[dependencies]
env_logger = "0.10"

// In your main function
env_logger::init();

Nightly Feature Debugging

# Enable detailed endpoint logging
SUPABASE_RS_NO_NIGHTLY_MSG=false

📈 Migration Guide

From v0.3.x to v0.4.x

Breaking Changes

  1. Method Signatures: Some methods now return Result<T, String> instead of Result<T, Error>
  2. Client Creation: new() method now returns Result<SupabaseClient, ErrorTypes>

Migration Steps

// Old (v0.3.x)
let client = SupabaseClient::new(url, key); // Could panic

// New (v0.4.x)
let client = SupabaseClient::new(url, key)?; // Returns Result

From v0.2.x to v0.3.x

Query Builder Changes

// Old
client.select("table").filter("column", "value")

// New
client.select("table").eq("column", "value")

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

Development Setup

  1. Clone the repository

    git clone https://github.com/floris-xlx/supabase_rs.git
    cd supabase_rs
    
  2. Set up environment

    cp .env.example .env
    # Edit .env with your Supabase credentials
    
  3. Run tests

    cargo test
    
  4. Check formatting and linting

    cargo fmt
    cargo clippy
    

Contribution Guidelines

  • Code Style: Follow Rust standard formatting (cargo fmt)
  • Documentation: Add comprehensive docs for all public APIs
  • Testing: Include tests for new functionality
  • Performance: Consider performance implications of changes
  • Compatibility: Maintain backward compatibility when possible

Areas for Contribution

  • Core Features: Improve existing CRUD operations
  • Storage: Enhance file upload capabilities
  • GraphQL: Stabilize GraphQL support
  • Documentation: Improve examples and guides
  • Testing: Add more comprehensive test coverage
  • Performance: Optimize query building and execution

Contributors

Special thanks to all contributors who have helped improve this project:

  • Hadi — Improved & fixed the schema-to-type generator
  • Izyuumi — Improved row ID routing with updating methods
  • koya1616 — README fixes and documentation improvements
  • strykejern — Refactoring & warning fixes

License

This project is licensed under the MIT License - see the LICENSE file for details.

Links