A canonical Salesforce Platform API client for Rust — built with production-grade safety, performance, and developer ergonomics.
force-rs provides idiomatic Rust bindings to the Salesforce Platform APIs, enabling you to build high-performance integrations, data pipelines, and automation tools. With comprehensive coverage of 7 API surfaces, compile-time safe workflows, and memory-efficient streaming, force-rs is designed for real-world enterprise workloads.
The workspace also includes force-sync, a Postgres-first bidirectional sync engine built on top of force and force-pubsub.
- CRUD Operations - Create, read, update, delete, and upsert records with full type safety
- SOQL Queries - Execute typed queries with automatic pagination and streaming results
- SOSL Search - Full-text search across multiple objects with builder pattern
- Metadata Access - Describe objects, fields, and org limits programmatically
- Relationship Support - Query parent-child and lookup relationships seamlessly
- Compile-Time Safety - Strict guarantees for job lifecycle (Open -> Upload -> InProgress -> Complete)
- Ingest Jobs - Insert, update, upsert, and delete millions of records efficiently
- Query Jobs - Execute bulk queries with streaming CSV results
- Memory Efficient - Stream large datasets without loading entire payloads into RAM
- Error Handling - Comprehensive job monitoring and failure analysis
- Batch Requests - Combine up to 25 subrequests in a single HTTP call
- Graph Requests - Up to 500 nodes with dependency ordering (feature:
composite_graph) - Reduced API Consumption - Minimize round trips and stay within governor limits
- Apex Management - Query and manage Apex classes, triggers, and components
- Execute Anonymous - Run Apex code on the fly with full result inspection
- Test Execution - Run Apex tests synchronously or asynchronously
- Code Completions - IDE-style completions for Apex and Visualforce
- Layout-Aware Records - Get presentation-ready data with display values and field visibility
- Object Metadata - Retrieve field info, picklist values, and record type mappings
- List Views - Access list view definitions, columns, and paginated records
- Lookups & Favorites - Type-ahead search and user favorite management
- Unified Queries - Request specific fields and nested relationships in a single call
- Typed Results - Deserialize into custom Rust structs or use dynamic
Value - Variables & Operations - Parameterized queries with named operations
- Partial Success Handling - Inspect both data and errors when both are present
- Multiple Auth Flows - JWT bearer, OAuth 2.0 client credentials
- Feature-Gated - Enable only the APIs you need for minimal binary size
- Async/Await - Built on Tokio for high-concurrency workloads
- Type-Safe Errors - Structured error types with context for debugging
- Production Ready - 870+ tests, zero clippy warnings, comprehensive examples
Add force-rs to your Cargo.toml:
[dependencies]
force = "0.1"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
anyhow = "1.0"
# Or enable specific features:
force = { version = "0.1", features = ["rest", "bulk", "jwt"] }Here's a minimal example using OAuth 2.0 client credentials to query Salesforce:
use force::auth::ClientCredentials;
use force::client::ForceClientBuilder;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Account {
#[serde(rename = "Id")]
id: String,
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Industry")]
industry: Option<String>,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Authenticate with OAuth 2.0 client credentials
let auth = ClientCredentials::new_my_domain(
"your-client-id",
"your-client-secret",
"https://your-org.my.salesforce.com",
);
let client = ForceClientBuilder::new()
.authenticate(auth)
.build()
.await?;
// Execute typed SOQL query
let soql = "SELECT Id, Name, Industry FROM Account WHERE Industry = 'Technology' LIMIT 10";
let result = client.rest().query::<Account>(soql).await?;
// Process results
for account in result.records {
println!("{}: {} ({})",
account.id,
account.name,
account.industry.unwrap_or_default()
);
}
Ok(())
}Note: If Salesforce returns a domain-support error for client-credentials auth, use your org's My Domain host, for example
https://your-org.my.salesforce.com.
Query specific fields and nested relationships in a single request:
// Requires the "graphql" feature: force = { version = "0.1", features = ["graphql"] }
use force::api::graphql::GraphqlRequest;
use force::auth::ClientCredentials;
use force::client::ForceClientBuilder;
use serde_json::json;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let auth = ClientCredentials::new_my_domain(
"client-id",
"client-secret",
"https://your-org.my.salesforce.com",
);
let client = ForceClientBuilder::new().authenticate(auth).build().await?;
let gql = client.graphql();
// Simple raw query
let data = gql.query_raw(
r#"{ uiapi { query { Account(first: 5) {
edges { node { Id Name { value } } }
totalCount
} } } }"#,
None,
).await?;
println!("Total: {}", data["uiapi"]["query"]["Account"]["totalCount"]);
// Query with variables
let req = GraphqlRequest::new("query($limit: Int) { uiapi { query { Account(first: $limit) { edges { node { Id } } } } } }")
.with_variables(json!({"limit": 10}))
.with_operation_name("GetAccounts");
let data: serde_json::Value = gql.query(&req).await?;
Ok(())
}The Bulk API uses Rust's type system to enforce the correct job lifecycle at compile time:
// Requires the "bulk" feature: force = { version = "0.1", features = ["bulk"] }
use force::client::ForceClientBuilder;
use force::auth::ClientCredentials;
use serde::Serialize;
#[derive(Serialize)]
struct Account {
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Industry")]
industry: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let auth = ClientCredentials::new_my_domain(
"client-id",
"client-secret",
"https://your-org.my.salesforce.com",
);
let client = ForceClientBuilder::new().authenticate(auth).build().await?;
let accounts = vec![
Account { name: "Acme Corp".into(), industry: "Technology".into() },
Account { name: "Global Ltd".into(), industry: "Manufacturing".into() },
];
// Convenience method handles: create job -> upload CSV -> close -> poll
let job_info = client.bulk().insert("Account", &accounts).await?;
println!("Processed: {}, Failed: {}",
job_info.number_records_processed.unwrap_or(0),
job_info.number_records_failed.unwrap_or(0)
);
Ok(())
}Stream millions of records without loading the entire dataset into memory:
// Requires the "bulk" feature: force = { version = "0.1", features = ["bulk"] }
use force::client::ForceClientBuilder;
use force::auth::ClientCredentials;
use serde::Deserialize;
// Requires `futures` crate
use futures::StreamExt;
#[derive(Debug, Deserialize)]
struct Contact {
#[serde(rename = "Id")]
id: String,
#[serde(rename = "Email")]
email: Option<String>,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let auth = ClientCredentials::new_my_domain(
"client-id",
"client-secret",
"https://your-org.my.salesforce.com",
);
let client = ForceClientBuilder::new().authenticate(auth).build().await?;
// Create bulk query job and stream results
let stream = client.bulk()
.query::<Contact>(
"SELECT Id, Email FROM Contact WHERE Email != null"
)
.await?;
let stream = stream.into_stream();
let mut stream = std::pin::pin!(stream);
let mut count = 0;
while let Some(contact_result) = stream.next().await {
let contact = contact_result?;
println!("Processing: {} ({})", contact.id, contact.email.unwrap_or_default());
count += 1;
}
println!("Streamed {} contacts", count);
Ok(())
}The examples/ directory contains comprehensive demonstrations:
| Example | Feature | Description |
|---|---|---|
basic_crud.rs |
rest |
Complete CRUD lifecycle (create, read, update, delete) |
soql_query.rs |
rest |
Typed queries with pagination and relationships |
dynamic_query.rs |
rest |
Dynamic queries without predefined types |
search.rs |
rest |
SOSL full-text search with builder pattern |
describe.rs |
rest |
Object and field metadata introspection |
org_limits.rs |
rest |
API limits and usage monitoring |
bulk_insert.rs |
bulk |
Bulk insert with job monitoring |
bulk_query.rs |
bulk |
Bulk query with streaming results |
bulk_update.rs |
bulk |
Bulk update operations |
bulk_delete.rs |
bulk |
Bulk delete with error handling |
tooling.rs |
tooling |
Apex classes, anonymous execution, completions, tests |
ui_api.rs |
ui |
Layout-aware records, object info, list views, favorites |
graphql.rs |
graphql |
GraphQL queries, typed results, variables, error handling |
soql_mass_op.rs |
composite |
Composite batch operations |
query_plan.rs |
rest |
SOQL query plan inspection |
Run any example with:
cargo run --example soql_query
cargo run --example bulk_insert --features bulk
cargo run --example graphql --features graphqlforce-rs uses feature flags to minimize dependencies and binary size:
| Feature | Description | Status |
|---|---|---|
rest |
REST API (CRUD, SOQL, SOSL, describe, limits) | Default |
bulk |
Bulk API 2.0 (ingest and query jobs) | Stable |
composite |
Composite API (batch requests) | Stable |
composite_graph |
Composite Graph API (dependency-ordered nodes) | Stable |
tooling |
Tooling API (Apex, execute anonymous, tests, completions) | Stable |
ui |
UI API (layout-aware records, object info, list views, favorites) | Stable |
graphql |
GraphQL API (queries, mutations, variables) | Stable |
jwt |
JWT bearer token authentication | Stable |
schema |
Schema analysis, scanning, and code generation utilities | Preview |
data_utility |
Mock-data generation and Salesforce seeding helpers | Preview |
mock |
Wiremock utilities for testing | Stable |
full |
All stable APIs (rest + bulk + composite + tooling + ui + graphql + jwt) |
Meta |
all |
Everything including preview features | Meta |
Recommendation: Start with default features, then add bulk and jwt as needed.
The force crate includes preview features for early adopters. These capabilities live in their real modules and remain feature-gated while the API settles before a future stabilization pass.
Available with the default
restfeature.
The Query Plan API allows you to inspect the performance cost of a SOQL query before executing it. This is useful for identifying inefficient queries (e.g., table scans) in CI/CD pipelines.
[dependencies]
force = "0.1"// Available with the default "rest" feature: force = "0.1"
use force::client::ForceClientBuilder;
use force::auth::ClientCredentials;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let auth = ClientCredentials::new_my_domain(
"client-id",
"client-secret",
"https://your-org.my.salesforce.com",
);
let client = ForceClientBuilder::new().authenticate(auth).build().await?;
let soql = "SELECT Id FROM Account WHERE Name LIKE 'A%'";
let explanation = client.rest().explain(soql).await?;
for plan in explanation.plans {
println!("Plan: {}, Cost: {}", plan.leading_operation_type, plan.relative_cost);
for note in plan.notes {
println!(" Note: {}", note.description);
}
}
Ok(())
}Preview utilities now live in the modules that own them:
force::api::composite::{QueryBatch, SoqlMassOp}: batch-aware helpers built on the Composite API.force::api::rest::analyze_query_plan: turnsexplain()responses into actionable warnings.force::schema: schema scanning, diffing, visualization, DDL export, and code generation helpers.force::data: mock-record generation and bulk seeding helpers.
force-rs is built around a handler pattern where each API surface gets its own feature-gated handler type:
ForceClient<A>
|-- .rest() -> RestHandler<A> (feature: rest)
|-- .bulk() -> BulkHandler<A> (feature: bulk)
|-- .composite() -> CompositeHandler<A> (feature: composite)
|-- .tooling() -> ToolingHandler<A> (feature: tooling)
|-- .ui() -> UiHandler<A> (feature: ui)
|-- .graphql() -> GraphqlHandler<A> (feature: graphql)
All handlers share a common Session<A> (via Arc) containing the HTTP client, token manager, and configuration. This ensures zero-cost handler creation and shared authentication state.
For the sync layer, see crates/force-sync and its design notes in docs/adr/026-force-sync-crate.md.
Architectural decisions are documented in docs/adr/:
| ADR | Decision |
|---|---|
| 001 | Workspace structure and module organization |
| 002 | Authentication trait design and flow support |
| 003 | Error hierarchy with thiserror |
| 004 | Feature flag strategy for API surfaces |
| 005 | Compile-time auth safety with phantom types |
| 006 | Handler pattern for API organization |
| 007 | REST API design decisions |
| 019 | RestOperation trait and Tooling API |
| 020 | UI API handler design |
| 021 | GraphQL API error handling strategy |
force-rs has comprehensive test coverage (870+ tests) using wiremock for HTTP mocking:
# Run all tests
cargo test --all-features
# Run with logging
RUST_LOG=debug cargo test --all-features
# Run specific API surface tests
cargo test --features graphql -- graphql
cargo test --features bulk -- bulkNightly live-contract tests (ignored by default in local runs) are available in CI and can be run manually with org credentials. OAuth URL env vars accept a bare host, an org base URL, or the full OAuth token endpoint; bare hosts are treated as HTTPS. Client-credentials live tests require SF_TOKEN_URL to be set explicitly for the target org/environment. Pub/Sub live tests require SF_PUBSUB_TOPIC to name an accessible event topic, for example /data/AccountChangeEvent; SF_PUBSUB_ENDPOINT defaults to https://api.pubsub.salesforce.com:7443.
- Auth Credential Rotation
- Rate-Limit Incident Response
- Retry and Polling Tuning
- Salesforce API Version Upgrade
- Documentation Index
The crate-level compatibility and feature-flag guarantees are documented in:
- Fast unit/lint/format gates for PR velocity
- Full test lanes for broader confidence
- Nightly live-contract workflow for real Salesforce contract validation
Contributions are welcome! force-rs follows strict TDD discipline and quality standards:
- Test-Driven Development - All features require failing tests first (RED -> GREEN -> REFACTOR)
- Code Quality -
cargo fmtandcargo clippy -- -D warningsmust pass - Documentation - All public APIs require doc comments with examples
- Architecture - ADRs (Architecture Decision Records) for significant changes
See CONTRIBUTING.md for detailed guidelines.
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
Built by the force-rs contributors | Documentation | Examples | Issues