Skip to main content
The official Rust SDK for TrailBase provides a type-safe, async client for accessing your TrailBase backend from Rust applications.

Installation

Add trailbase-client to your Cargo.toml:
Cargo.toml
[dependencies]
trailbase-client = "0.5.1"

Features

  • ws - Enable WebSocket support for subscriptions (optional)
Cargo.toml
[dependencies]
trailbase-client = { version = "0.5.1", features = ["ws"] }

Initialization

Basic Client

use trailbase_client::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new("https://your-server.trailbase.io").await?;
    Ok(())
}

Client with Tokens

use trailbase_client::{Client, Tokens};

let tokens = Tokens {
    auth_token: "your-auth-token".to_string(),
    refresh_token: Some("your-refresh-token".to_string()),
    csrf_token: Some("your-csrf-token".to_string()),
};

let client = Client::with_tokens(
    "https://your-server.trailbase.io",
    tokens
).await?;

Authentication

Login

let tokens = client.login("[email protected]", "password").await?;
println!("Auth token: {}", tokens.auth_token);

let user = client.user();
if let Some(user) = user {
    println!("Logged in as: {}", user.email);
}

Logout

client.logout().await?;

Current User

if let Some(user) = client.user() {
    println!("User ID: {}", user.sub);
    println!("Email: {}", user.email);
}

Access Tokens

if let Some(tokens) = client.tokens() {
    // Persist tokens for later use
    let json = serde_json::to_string(&tokens)?;
    std::fs::write("tokens.json", json)?;
}

Record API

Define Your Record Types

use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Post {
    id: String,
    title: String,
    content: String,
    author_id: String,
    created_at: i64,
}

#[derive(Debug, Serialize, Deserialize)]
struct NewPost {
    title: String,
    content: String,
}

List Records

use trailbase_client::{ListArguments, Pagination};

let posts = client.records::<Post>("posts");

let args = ListArguments::new()
    .with_pagination(Pagination::new().with_limit(10))
    .with_order(&["-created_at"])
    .with_count(true);

let response = posts.list(args).await?;

println!("Records: {}", response.records.len());
println!("Total count: {:?}", response.total_count);
println!("Next cursor: {:?}", response.cursor);

Read a Record

// Simple read with string ID
let post: Post = posts.read("post-id").await?;

// Read with integer ID
let post: Post = posts.read(123i64).await?;

// With expanded relationships
use trailbase_client::ReadArguments;

let args = ReadArguments::new("post-id")
    .with_expand(&["author"]);
let post: Post = posts.read(args).await?;

println!("Title: {}", post.title);

Create a Record

let new_post = NewPost {
    title: "Hello World".to_string(),
    content: "My first post from Rust".to_string(),
};

let post_id = posts.create(&new_post).await?;
println!("Created post with ID: {}", post_id);

Update a Record

use serde_json::json;

let update = json!({
    "title": "Updated Title"
});

posts.update("post-id", &update).await?;

Delete a Record

posts.delete("post-id").await?;

Filtering

use trailbase_client::{Filter, CompareOp, ValueOrFilterGroup};

// Simple equality filter
let args = ListArguments::new()
    .with_filter(Filter::new("author_id", "user-123"));

let response = posts.list(args).await?;

// With comparison operators
let args = ListArguments::new()
    .with_filter(
        Filter::new("created_at", &week_ago.to_string())
            .with_op(CompareOp::GreaterThan)
    );

// LIKE operator for text search
let args = ListArguments::new()
    .with_filter(
        Filter::new("title", "%search%")
            .with_op(CompareOp::Like)
    );

// AND composite filter
use trailbase_client::FilterGroup;

let filters = FilterGroup::And(vec![
    ValueOrFilterGroup::Value(Filter::new("status", "published")),
    ValueOrFilterGroup::Value(Filter::new("author_id", user_id)),
]);

let args = ListArguments::new()
    .with_filters(Some(filters));

Available Comparison Operators

pub enum CompareOp {
    Equal,
    NotEqual,
    GreaterThanEqual,
    GreaterThan,
    LessThanEqual,
    LessThan,
    Like,
    Regexp,
    StWithin,      // Geospatial
    StIntersects,  // Geospatial
    StContains,    // Geospatial
}

Real-time Subscriptions

Subscribe to Record Changes

use futures_lite::StreamExt;
use trailbase_client::DbEvent;

// Subscribe to a single record
let mut stream = posts.subscribe("post-id").await?;

while let Some(event) = stream.next().await {
    match event {
        DbEvent::Insert(value) => {
            println!("Record inserted: {:?}", value);
        }
        DbEvent::Update(value) => {
            println!("Record updated: {:?}", value);
        }
        DbEvent::Delete(value) => {
            println!("Record deleted: {:?}", value);
        }
        DbEvent::Error(msg) => {
            eprintln!("Subscription error: {}", msg);
        }
    }
}

Subscribe to All Records

let filters = Some(Filter::new("author_id", user_id));
let mut stream = posts.subscribe_all(filters).await?;

while let Some(event) = stream.next().await {
    println!("Change event: {:?}", event);
}

WebSocket Subscriptions

With the ws feature enabled:
#[cfg(feature = "ws")]
{
    let mut stream = posts.subscribe_ws("post-id", None).await?;
    
    while let Some(event) = stream.next().await {
        println!("WebSocket event: {:?}", event);
    }
}

Error Handling

use trailbase_client::Error;

match posts.read("post-id").await {
    Ok(post) => println!("Post: {:?}", post),
    Err(Error::HttpStatus(status)) => {
        eprintln!("HTTP error: {}", status);
    }
    Err(Error::MissingRefreshToken) => {
        eprintln!("No refresh token available");
    }
    Err(e) => {
        eprintln!("Error: {}", e);
    }
}

Type Definitions

User

pub struct User {
    pub sub: String,
    pub email: String,
}

Tokens

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Tokens {
    pub auth_token: String,
    pub refresh_token: Option<String>,
    pub csrf_token: Option<String>,
}

ListResponse

#[derive(Clone, Debug, Deserialize)]
pub struct ListResponse<T> {
    pub cursor: Option<String>,
    pub total_count: Option<usize>,
    pub records: Vec<T>,
}

Pagination

#[derive(Clone, Debug, Default, PartialEq)]
pub struct Pagination {
    // Private fields
}

impl Pagination {
    pub fn new() -> Self;
    pub fn with_limit(self, limit: impl Into<Option<usize>>) -> Pagination;
    pub fn with_cursor(self, cursor: impl Into<Option<String>>) -> Pagination;
    pub fn with_offset(self, offset: impl Into<Option<usize>>) -> Pagination;
}

DbEvent

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DbEvent {
    Update(Option<serde_json::Value>),
    Insert(Option<serde_json::Value>),
    Delete(Option<serde_json::Value>),
    Error(String),
}

RecordId Trait

The RecordId trait allows using different types as record identifiers:
use trailbase_client::RecordId;

// String
let post: Post = posts.read("post-id").await?;

// &str
let post: Post = posts.read("post-id").await?;

// i64
let post: Post = posts.read(123i64).await?;

// String (owned)
let id = String::from("post-id");
let post: Post = posts.read(id).await?;

Best Practices

Use the #[derive(Serialize, Deserialize)] macros on your record types for seamless serialization.
Store tokens securely and never commit them to version control. Consider using environment variables or secure credential storage.
The client automatically refreshes auth tokens before they expire. Manual refresh is rarely needed.

Example Application

use trailbase_client::{Client, Filter, ListArguments, Pagination};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Post {
    id: String,
    title: String,
    content: String,
    published: bool,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize client
    let client = Client::new(
        std::env::var("TRAILBASE_URL")
            .unwrap_or_else(|_| "http://localhost:4000".to_string())
    ).await?;
    
    // Login
    client.login(
        &std::env::var("TRAILBASE_EMAIL")?,
        &std::env::var("TRAILBASE_PASSWORD")?,
    ).await?;
    
    if let Some(user) = client.user() {
        println!("Logged in as: {}", user.email);
    }
    
    // List posts
    let posts = client.records::<Post>("posts");
    
    let args = ListArguments::new()
        .with_pagination(Pagination::new().with_limit(10))
        .with_order(&["-created_at"])
        .with_filter(Filter::new("published", "true"));
    
    let response = posts.list(args).await?;
    
    println!("\nFound {} posts:", response.records.len());
    for post in &response.records {
        println!("- {}", post.title);
    }
    
    // Create a new post
    let new_post = serde_json::json!({
        "title": "Hello from Rust",
        "content": "This post was created using the TrailBase Rust SDK",
        "published": true
    });
    
    let new_post_id = posts.create(&new_post).await?;
    println!("\nCreated new post with ID: {}", new_post_id);
    
    // Read the post
    let post: Post = posts.read(&new_post_id).await?;
    println!("Post title: {}", post.title);
    
    // Logout
    client.logout().await?;
    println!("\nLogged out");
    
    Ok(())
}

Build docs developers (and LLMs) love