Skip to main content

supabase_rs/
lib.rs

1//! # Supabase SDK for Rust
2//!
3//! An unofficial, lightweight Rust SDK for [Supabase](https://supabase.io/) that provides a clean,
4//! type-safe interface for interacting with Supabase's REST and GraphQL APIs.
5//!
6//! This crate focuses on developer experience with a fluent, chainable API design that feels natural
7//! in Rust while maintaining compatibility with Supabase's PostgREST conventions.
8//!
9//! ## 🚀 Core Features
10//!
11//! ### Database Operations
12//! - **[`Insert`](insert)**: Add new rows with automatic ID generation and conflict handling
13//! - **[`Insert if unique`](insert)**: Conditional inserts with uniqueness validation
14//! - **[`Update`](update)**: Modify existing rows by ID or custom columns
15//! - **[`Upsert`](update)**: Insert or update with conflict resolution
16//! - **[`Select`](select)**: Retrieve data with advanced filtering and pagination
17//! - **[`Delete`](delete)**: Remove rows by ID or custom criteria
18//!
19//! ### Query Building
20//! - **Fluent API**: Chain filters, sorts, and pagination naturally
21//! - **Type Safety**: Leverage Rust's type system for compile-time guarantees
22//! - **Performance**: Built-in connection pooling and efficient query construction
23//!
24//! ### Advanced Features
25//! - **[`Storage`](storage)**: File upload/download operations (feature-gated)
26//! - **[`GraphQL`](graphql)**: Advanced querying with GraphQL (experimental)
27//! - **[`Realtime`](realtime)**: Live data subscriptions (planned)
28//!
29//! ## 🎯 Feature Flags
30//!
31//! | Feature | Description | Stability |
32//! |---------|-------------|-----------|
33//! | `storage` | File operations with Supabase Storage | ✅ Stable |
34//! | `rustls` | Use rustls instead of OpenSSL for TLS | ✅ Stable |
35//! | `nightly` | Experimental GraphQL support | ⚠️ Experimental |
36//!
37//! ### Feature Flag Details
38//!
39//! - **`storage`**: Enables the [`storage`] module for file upload/download operations
40//! - **`rustls`**: Forces the HTTP client to use `rustls` instead of OpenSSL (recommended for Alpine Linux)
41//! - **`nightly`**: Unlocks experimental GraphQL capabilities with detailed debugging
42//!
43//! ## ⚠️ Nightly Features
44//!
45//! Nightly features are experimental and may introduce breaking changes without notice.
46//! Use with caution in production environments.
47//!
48//! To disable nightly warning messages:
49//! ```env
50//! SUPABASE_RS_NO_NIGHTLY_MSG=true
51//! ```
52//!
53//! ## 🏗️ Architecture Overview
54//!
55//! The SDK is built around a central [`SupabaseClient`] that manages:
56//! - HTTP connection pooling via [`reqwest::Client`]
57//! - Authentication headers and API key management
58//! - Endpoint URL construction and routing
59//! - Request/response serialization
60//!
61//! ### Module Organization
62//!
63//! ```text
64//! supabase_rs/
65//! ├── lib.rs           # Main client and public API
66//! ├── insert.rs        # Insert operations and bulk operations
67//! ├── update.rs        # Update and upsert operations
68//! ├── select.rs        # Query execution and response handling
69//! ├── delete.rs        # Delete operations
70//! ├── query_builder/   # Fluent query building
71//! │   ├── builder.rs   # QueryBuilder implementation
72//! │   ├── filter.rs    # Filter operations (eq, gt, lt, etc.)
73//! │   └── sort.rs      # Sorting and ordering
74//! ├── storage/         # File operations (feature-gated)
75//! ├── graphql/         # GraphQL support (experimental)
76//! ├── errors.rs        # Error types and handling
77//! └── request/         # HTTP request utilities
78//! ```
79//!
80//! ## 📦 Installation
81//!
82//! Add to your `Cargo.toml`:
83//! ```toml
84//! [dependencies]
85//! supabase_rs = "0.4.14"
86//!
87//! # With optional features
88//! supabase_rs = { version = "0.4.14", features = ["storage", "rustls"] }
89//! ```
90//!
91//! ## 🚀 Quick Start
92//!
93//! ```rust,no_run
94//! use supabase_rs::SupabaseClient;
95//! use serde_json::json;
96//!
97//! #[tokio::main]
98//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
99//!     // Initialize client
100//!     let client = SupabaseClient::new(
101//!         std::env::var("SUPABASE_URL")?,
102//!         std::env::var("SUPABASE_KEY")?,
103//!     )?;
104//!
105//!     // Insert data
106//!     let id = client.insert("users", json!({
107//!         "name": "John Doe",
108//!         "email": "john@example.com"
109//!     })).await?;
110//!
111//!     // Query data
112//!     let users = client
113//!         .select("users")
114//!         .eq("name", "John Doe")
115//!         .limit(10)
116//!         .execute()
117//!         .await?;
118//!
119//!     println!("Found {} users", users.len());
120//!     Ok(())
121//! }
122//! ```
123//!
124//! ## 📚 Core Concepts
125//!
126//! ### Client Initialization
127//!
128//! The [`SupabaseClient`] is the main entry point for all operations. It's designed to be:
129//! - **Clone-friendly**: Cheap to clone, shares connection pool
130//! - **Thread-safe**: Can be used across async tasks
131//! - **Connection-pooled**: Reuses HTTP connections efficiently
132//!
133//! ### Query Builder Pattern
134//!
135//! The SDK uses a fluent query builder pattern for constructing complex queries:
136//!
137//! ```rust,no_run
138//! use supabase_rs::SupabaseClient;
139//! use serde_json::Value;
140//!
141//! # async fn example(client: SupabaseClient) -> Result<(), String> {
142//! let results: Vec<Value> = client
143//!     .from("posts")                    // Start with table
144//!     .columns(vec!["id", "title"])     // Select specific columns
145//!     .eq("status", "published")        // Add filters
146//!     .gte("created_at", "2024-01-01")  // Multiple filters
147//!     .order("created_at", false)       // Sort by date, newest first
148//!     .limit(20)                        // Limit results
149//!     .execute()                        // Execute query
150//!     .await?;
151//! # Ok(())
152//! # }
153//! ```
154//!
155//! ### Error Handling Philosophy
156//!
157//! The SDK uses `Result<T, String>` for most operations to provide clear error messages:
158//!
159//! ```rust,no_run
160//! # use supabase_rs::SupabaseClient;
161//! # use serde_json::json;
162//! # async fn example(client: SupabaseClient) -> Result<(), String> {
163//! match client.insert("users", json!({"email": "test@example.com"})).await {
164//!     Ok(id) => println!("Created user with ID: {}", id),
165//!     Err(err) => {
166//!         if err.contains("409") {
167//!             println!("User already exists");
168//!         } else {
169//!             println!("Unexpected error: {}", err);
170//!         }
171//!     }
172//! }
173//! # Ok(())
174//! # }
175//! ```
176//!
177//! ## 🔐 Authentication & Setup
178//!
179//! The SDK requires two pieces of information to connect to your Supabase project:
180//! - **Project URL**: Your unique Supabase project URL
181//! - **API Key**: Either your anon key (client-side) or service role key (server-side)
182//!
183//! ### Environment Configuration
184//!
185//! Set up your environment variables in a `.env` file:
186//! ```env
187//! SUPABASE_URL=https://your-project.supabase.co
188//! SUPABASE_KEY=your-anon-or-service-role-key
189//! ```
190//!
191//! ### Key Types and Usage
192//!
193//! | Key Type | Use Case | Permissions |
194//! |----------|----------|-------------|
195//! | **Anon Key** | Client-side apps | Respects RLS policies |
196//! | **Service Role** | Server-side apps | Bypasses RLS, full access |
197//!
198//! ## 📖 Complete Examples
199//!
200//! ### Client Initialization Patterns
201//!
202//! ```rust,no_run
203//! use supabase_rs::SupabaseClient;
204//! use dotenv::dotenv;
205//!
206//! // Basic initialization with error handling
207//! fn create_client() -> Result<SupabaseClient, Box<dyn std::error::Error>> {
208//!     dotenv().ok();
209//!     
210//!     let client = SupabaseClient::new(
211//!         std::env::var("SUPABASE_URL")?,
212//!         std::env::var("SUPABASE_KEY")?,
213//!     )?;
214//!     
215//!     Ok(client)
216//! }
217//!
218//! // For applications that need shared client instances
219//! use std::sync::Arc;
220//!
221//! fn create_shared_client() -> Arc<SupabaseClient> {
222//!     let client = SupabaseClient::new(
223//!         std::env::var("SUPABASE_URL").expect("SUPABASE_URL required"),
224//!         std::env::var("SUPABASE_KEY").expect("SUPABASE_KEY required"),
225//!     ).expect("Failed to create Supabase client");
226//!     
227//!     Arc::new(client)
228//! }
229//! ```
230//!
231//! ### Insert Operations
232//!
233//! ```rust,no_run
234//! use supabase_rs::SupabaseClient;
235//! use serde_json::json;
236//!
237//! # async fn example(client: SupabaseClient) -> Result<(), String> {
238//! // Basic insert with automatic ID generation
239//! let user_id = client.insert("users", json!({
240//!     "name": "Alice Johnson",
241//!     "email": "alice@example.com",
242//!     "age": 28
243//! })).await?;
244//!
245//! println!("Created user with ID: {}", user_id);
246//!
247//! // Insert with uniqueness check (prevents duplicates)
248//! let unique_id = client.insert_if_unique("users", json!({
249//!     "email": "unique@example.com",
250//!     "username": "unique_user"
251//! })).await?;
252//!
253//! // Bulk insert for multiple records
254//! use serde::Serialize;
255//!
256//! #[derive(Serialize)]
257//! struct NewUser {
258//!     name: String,
259//!     email: String,
260//! }
261//!
262//! let users = vec![
263//!     NewUser { name: "Bob".to_string(), email: "bob@example.com".to_string() },
264//!     NewUser { name: "Carol".to_string(), email: "carol@example.com".to_string() },
265//! ];
266//!
267//! client.bulk_insert("users", users).await?;
268//! # Ok(())
269//! # }
270//! ```
271//!
272//! ### Update & Upsert Operations
273//!
274//! ```rust,no_run
275//! # use supabase_rs::SupabaseClient;
276//! # use serde_json::json;
277//! # async fn example(client: SupabaseClient) -> Result<(), String> {
278//! // Update existing record by ID
279//! client.update("users", "123", json!({
280//!     "name": "Alice Smith",
281//!     "last_login": "2024-01-15T10:30:00Z"
282//! })).await?;
283//!
284//! // Update by custom column
285//! client.update_with_column_name(
286//!     "users",
287//!     "email",                    // Column to match
288//!     "alice@example.com",        // Value to match
289//!     json!({ "verified": true })
290//! ).await?;
291//!
292//! // Upsert (insert or update if exists)
293//! client.upsert("settings", "user_123", json!({
294//!     "theme": "dark",
295//!     "notifications": true
296//! })).await?;
297//! # Ok(())
298//! # }
299//! ```
300//!
301//! ### Query Operations
302//!
303//! ```rust,no_run
304//! # use supabase_rs::SupabaseClient;
305//! # use serde_json::Value;
306//! # async fn example(client: SupabaseClient) -> Result<(), String> {
307//! // Basic select with filtering
308//! let active_users: Vec<Value> = client
309//!     .select("users")
310//!     .eq("status", "active")
311//!     .order("created_at", false)     // Newest first
312//!     .limit(50)
313//!     .execute()
314//!     .await?;
315//!
316//! // Select specific columns (more efficient)
317//! let user_emails: Vec<Value> = client
318//!     .from("users")
319//!     .columns(vec!["id", "email", "name"])
320//!     .gte("age", "18")               // Adults only
321//!     .execute()
322//!     .await?;
323//!
324//! // Complex filtering with multiple conditions
325//! let filtered_posts: Vec<Value> = client
326//!     .select("posts")
327//!     .eq("published", "true")
328//!     .in_("category", &["tech", "science", "programming"])
329//!     .text_search("content", "rust programming")
330//!     .limit(10)
331//!     .execute()
332//!     .await?;
333//!
334//! // Pagination using range (recommended)
335//! let page_1: Vec<Value> = client
336//!     .from("articles")
337//!     .range(0, 24)                   // First 25 items (0-24 inclusive)
338//!     .order("published_at", false)
339//!     .execute()
340//!     .await?;
341//! # Ok(())
342//! # }
343//! ```
344//!
345//! ### Delete Operations
346//!
347//! ```rust,no_run
348//! # use supabase_rs::SupabaseClient;
349//! # async fn example(client: SupabaseClient) -> Result<(), String> {
350//! // Delete by ID
351//! client.delete("users", "123").await?;
352//!
353//! // Delete by custom column
354//! client.delete_without_defined_key("sessions", "token", "abc123").await?;
355//! # Ok(())
356//! # }
357//! ```
358//!
359//! ### Count Operations
360//!
361//! > **⚠️ Performance Warning**: Count operations can be expensive on large tables.
362//!
363//! ```rust,no_run
364//! # use supabase_rs::SupabaseClient;
365//! # async fn example(client: SupabaseClient) -> Result<(), String> {
366//! // Count all records (expensive)
367//! let total = client
368//!     .select("users")
369//!     .count()
370//!     .execute()
371//!     .await?;
372//!
373//! // Count with filters (more efficient)
374//! let active_count = client
375//!     .select("users")
376//!     .eq("status", "active")
377//!     .count()
378//!     .execute()
379//!     .await?;
380//! # Ok(())
381//! # }
382//! ```
383//!
384//! ## 🔗 Module Documentation
385//!
386//! For detailed documentation on specific functionality:
387//!
388//! - **[`insert`]** - Insert operations and bulk operations
389//! - **[`update`]** - Update and upsert operations  
390//! - **[`select`]** - Query execution and response handling
391//! - **[`delete`]** - Delete operations
392//! - **[`query_builder`]** - Fluent query building API
393//! - **[`storage`]** - File operations (requires `storage` feature)
394//! - **[`graphql`]** - GraphQL support (requires `nightly` feature)
395//! - **[`errors`]** - Error types and handling utilities
396//!
397//! ## 🚀 What's Next
398//!
399//! This SDK is actively maintained and continuously improved. Upcoming features include:
400//! - Enhanced Realtime subscriptions
401//! - Advanced authentication helpers
402//! - Improved type generation utilities
403//! - Performance optimizations
404//!
405//! ## 🤝 Contributing
406//!
407//! Contributions are welcome! Please check our [GitHub repository](https://github.com/floris-xlx/supabase_rs)
408//! for contribution guidelines and open issues.
409
410const PKG_NAME: &str = env!("CARGO_PKG_NAME");
411const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
412
413use rand::prelude::ThreadRng;
414use rand::RngExt;
415use reqwest::Client;
416
417pub mod delete;
418pub mod errors;
419pub mod insert;
420pub mod query;
421pub mod query_builder;
422pub mod request;
423pub mod routing;
424pub mod select;
425pub mod success;
426#[cfg(test)]
427pub mod tests;
428pub mod type_gen;
429pub mod update;
430
431// Re-export commonly used types
432pub use success::SupabaseErrorResponse;
433
434pub mod graphql;
435pub mod nightly;
436
437// This is locked by feature flag `storage` & `realtime`
438pub mod realtime;
439pub mod storage;
440
441// This is locked by feature flag `rpc`
442pub mod rpc;
443
444use errors::Result;
445
446/// The main client for interacting with Supabase services.
447///
448/// `SupabaseClient` provides a unified interface for all Supabase operations including
449/// database CRUD operations, file storage, and GraphQL queries. It manages HTTP connections,
450/// authentication, and request routing automatically.
451///
452/// # Architecture
453///
454/// The client is built around several key components:
455/// - **Connection Pool**: Managed by an internal `reqwest::Client` for efficient HTTP reuse
456/// - **Authentication**: Automatic header management with API key and bearer token
457/// - **Endpoint Routing**: Smart URL construction for different Supabase services
458/// - **Error Handling**: Consistent error types across all operations
459///
460/// # Thread Safety & Performance
461///
462/// - **Clone-friendly**: Cloning is cheap and shares the underlying connection pool
463/// - **Thread-safe**: Can be safely used across async tasks and threads
464/// - **Connection pooling**: Automatically reuses HTTP connections for better performance
465/// - **Memory efficient**: Minimal overhead per clone
466///
467/// # TLS Configuration
468///
469/// - **Default**: Uses the system's native TLS implementation (OpenSSL on most platforms)
470/// - **With `rustls` feature**: Uses rustls for TLS (recommended for Alpine Linux/Docker)
471///
472/// # Examples
473///
474/// ## Basic Usage
475/// ```rust,no_run
476/// use supabase_rs::SupabaseClient;
477///
478/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
479/// let client = SupabaseClient::new(
480///     "https://your-project.supabase.co",
481///     "your-secret-key",
482/// )?;
483/// # Ok(())
484/// # }
485/// ```
486///
487/// ## Multi-threaded Usage
488/// ```rust,no_run
489/// use supabase_rs::SupabaseClient;
490/// use std::sync::Arc;
491/// use tokio::task;
492///
493/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
494/// let client = Arc::new(SupabaseClient::new(
495///     std::env::var("SUPABASE_URL")?,
496///     std::env::var("SUPABASE_KEY")?,
497/// )?);
498///
499/// // Clone for use in another task
500/// let client_clone = Arc::clone(&client);
501/// let handle = task::spawn(async move {
502///     client_clone.select("users").execute().await
503/// });
504///
505/// // Original client can still be used
506/// let _users = client.select("posts").execute().await?;
507/// let _result = handle.await??;
508/// # Ok(())
509/// # }
510/// ```
511#[derive(Debug, Clone)]
512pub struct SupabaseClient {
513    url: String,
514    api_key: String,
515    schema: String,
516    client: reqwest::Client,
517}
518
519impl SupabaseClient {
520    /// Creates a new `SupabaseClient` instance with the provided project URL and API key.
521    ///
522    /// This method initializes the HTTP client with appropriate TLS configuration based on
523    /// enabled features and sets up the authentication credentials for all subsequent requests.
524    ///
525    /// # Arguments
526    ///
527    /// * `supabase_url` - Your Supabase project URL (e.g., "https://your-project.supabase.co")
528    /// * `private_key` - Your Supabase API key (anon key for client-side, service role for server-side)
529    ///
530    /// # Returns
531    ///
532    /// Returns `Result<SupabaseClient, ErrorTypes>` where:
533    /// - `Ok(SupabaseClient)` - Successfully initialized client ready for use
534    /// - `Err(ErrorTypes)` - Initialization failed (typically due to HTTP client setup issues)
535    ///
536    /// # TLS Configuration
537    ///
538    /// - **Default**: Uses native TLS (OpenSSL on most platforms)
539    /// - **With `rustls` feature**: Uses rustls-tls for cross-platform compatibility
540    ///
541    /// # Examples
542    ///
543    /// ## Basic Initialization
544    /// ```rust,no_run
545    /// use supabase_rs::SupabaseClient;
546    ///
547    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
548    /// let client = SupabaseClient::new(
549    ///     "https://your-project.supabase.co",
550    ///     "your-anon-or-service-key",
551    /// )?;
552    /// # Ok(())
553    /// # }
554    /// ```
555    ///
556    /// ## With Environment Variables
557    /// ```rust,no_run
558    /// use supabase_rs::SupabaseClient;
559    /// use dotenv::dotenv;
560    ///
561    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
562    /// dotenv().ok();
563    ///
564    /// let client = SupabaseClient::new(
565    ///     std::env::var("SUPABASE_URL")?,
566    ///     std::env::var("SUPABASE_KEY")?,
567    /// )?;
568    /// # Ok(())
569    /// # }
570    /// ```
571    ///
572    /// ## Error Handling
573    /// ```rust,no_run
574    /// use supabase_rs::SupabaseClient;
575    ///
576    /// # fn main() {
577    /// match SupabaseClient::new("invalid-url", "key") {
578    ///     Ok(client) => println!("Client created successfully"),
579    ///     Err(e) => eprintln!("Failed to create client: {:?}", e),
580    /// }
581    /// # }
582    /// ```
583    pub fn new(supabase_url: impl Into<String>, private_key: impl Into<String>) -> Result<Self> {
584        #[cfg(feature = "rustls")]
585        let client = Client::builder().use_rustls_tls().build()?;
586
587        #[cfg(not(feature = "rustls"))]
588        let client = Client::new();
589
590        Ok(Self {
591            url: supabase_url.into(),
592            api_key: private_key.into(),
593            schema: "public".to_owned(), // default schema
594            client,
595        })
596    }
597
598    pub fn schema(mut self, schema: &str) -> Self {
599        self.schema = schema.to_owned();
600        self
601    }
602
603    /// Calls a Postgres RPC function.
604    ///
605    /// # Arguments
606    /// * `function_name` - The name of the RPC function to call.
607    /// * `params` - The arguments to pass to the function. Can be a struct, map, or `json!({})`.
608    ///
609    /// # Returns
610    /// Returns a `RpcBuilder` for further chaining (filtering) or execution.
611    #[cfg(feature = "rpc")]
612    pub fn rpc<T>(&self, function_name: &str, params: T) -> crate::rpc::RpcBuilder
613    where
614        T: serde::Serialize,
615    {
616        crate::rpc::RpcBuilder::new(self.clone(), function_name, params)
617    }
618
619    /// Returns the base URL of the Supabase project and table.
620    ///
621    /// # Arguments
622    /// * `table_name` - The name of the table that will be used.
623    ///
624    /// # Returns
625    /// Returns a string containing the endpoint URL.
626    ///
627    /// The default format is `"{url}/rest/v1/{table}"`. If the environment variable
628    /// `SUPABASE_RS_DONT_REST_V1_URL=true` is set, it becomes `"{url}/{table}"`.
629    fn endpoint(&self, table_name: &str) -> String {
630        let dont_use_rest_v1: bool = std::env::var("SUPABASE_RS_DONT_REST_V1_URL")
631            .map(|val| val.to_lowercase() == "true")
632            .unwrap_or(false);
633
634        if dont_use_rest_v1 {
635            format!("{}/{}", self.url, table_name)
636        } else {
637            format!("{}/rest/v1/{}", self.url, table_name)
638        }
639    }
640
641    /// Returns the RPC endpoint URL for a given function name.
642    ///
643    /// # Arguments
644    /// * `function_name` - The name of the RPC function to call.
645    ///
646    /// # Returns
647    /// Returns a string containing the RPC endpoint URL.
648    ///
649    /// The default format is `"{url}/rest/v1/rpc/{function_name}"`. If the environment variable
650    /// `SUPABASE_RS_DONT_REST_V1_URL=true` is set, it becomes `"{url}/rpc/{function_name}"`.
651    pub(crate) fn rpc_endpoint(&self, function_name: &str) -> String {
652        let dont_use_rest_v1: bool = std::env::var("SUPABASE_RS_DONT_REST_V1_URL")
653            .map(|val| val.to_lowercase() == "true")
654            .unwrap_or(false);
655
656        if dont_use_rest_v1 {
657            format!("{}/rpc/{}", self.url, function_name)
658        } else {
659            format!("{}/rest/v1/rpc/{}", self.url, function_name)
660        }
661    }
662}
663
664/// Generates a random 64-bit signed integer within a larger range.
665///
666/// This is used by insert helpers that need a default `id` value.
667/// The range is `[0, i64::MAX)`, uniform from `rand`.
668///
669/// # Examples
670/// ```
671/// let id = supabase_rs::generate_random_id();
672/// assert!(id >= 0);
673/// ```
674pub fn generate_random_id() -> i64 {
675    let mut rng: ThreadRng = rand::rng();
676    rng.random_range(0..i64::MAX)
677}
678
679/// Returns an identifier string `{package-name}/{package-version}` used for a `Client-Info` header.
680pub(crate) fn client_info() -> String {
681    format!("{}/{PKG_VERSION}", PKG_NAME.replace("_", "-"))
682}