SDKs & ToolsRust SDK

Client

Configure the Rust SDK client with an endpoint, a credential, and the embedded backend.

In development

A Client is your application's connection to Spiral. Create one when your application starts and share it: it holds one HTTP connection pool, one credential source, and the backend that reads and writes Table data. Clones of a client share all three, so cloning is cheap.

Connect with a token

Give the builder the API endpoint and a bearer token:

let client = Client::builder()
    .api_url("https://api.spiraldb.dev")
    .bearer_token(token)
    .build()?;

To get a token for a user or a Workload, see Authentication. If your tokens expire while the application runs, pass an token_source instead of a fixed bearer_token. The client calls the provider before every request, so it always sends a current token.

Builder methodSets
api_urlThe Spiral API endpoint. Required.
bearer_tokenA fixed bearer token.
token_sourceA provider that the client calls for a token before every request.
http_clientA reqwest::Client, to share one connection pool with the rest of your application.
embedded_scandalRun Table operations in ScanDAL linked from the native archive, which reads spiral:// addresses as the client's identity. Build the client inside a Tokio runtime.
scandalWhere Table operations run: a ScanDAL you construct, such as a NativeScandal.
scandal_urlThe endpoint of a remote ScanDAL service that runs Table operations.

build fails if an endpoint is not an HTTP or HTTPS URL, or if it contains credentials, a query, or a fragment.

Discover workload credentials

Configure a Workload, or supply a bearer token in SPIRAL_TOKEN or with Config::builder. The client selects those credentials and refreshes renewable providers as needed. Enable the feature for the host platform, such as gcp or aws, to discover its workload identity. workload-auth alone enables custom OIDC discovery and shared refresh helpers; cloud identity providers are opt-in:

let client = Client::from_env().await?;

Client::from_env is asynchronous because it resolves credentials and discovers the API endpoint before returning. It never starts a browser or asks you to sign in. If no credentials are available, it returns CredentialsNotFound.

The default credential chain uses these sources in order:

  1. SPIRAL_TOKEN.
  2. With private-key-auth, a default feature, SPIRAL_CLIENT_ID and SPIRAL_PRIVATE_KEY.
  3. SPIRAL_CLIENT_ID and SPIRAL_CLIENT_SECRET for an older Workload.
  4. With workload-auth, SPIRAL_WORKLOAD_ID and a custom OIDC assertion or an enabled platform identity from Kubernetes, Modal, GitHub Actions, GCP or AWS.
  5. An optional provider supplied with fallback_token_source.

A configured credential failure stops discovery. After selecting a source, the client keeps using it when credentials expire. It does not fall back to a different account if renewal fails. Concurrent requests share discovery and refresh.

The SDK does not read a settings file such as ~/.spiral.toml. Your application reads its own configuration and passes the endpoints to Config::builder or Client::builder. Without an explicit API endpoint, the client uses the endpoint returned with the credential, else spiral::config::DEFAULT_API_URL. An explicit API endpoint remains selected even when the token names another endpoint. SPIRAL_AUTH_URL selects the authorization server that discovered credentials use. See Authentication for the sign-in methods.

Share configuration

Load configuration once when several clients need the same credentials and connection pool:

let config = Config::load().await?;
let client = Client::from_config(&config)?;

Use Client::builder().config(&config) when selecting an embedded backend or another client option. The default chain also applies to clients built with default-features = false; resource management needs no native library.

A caller-supplied fallback controls its own refresh and interactive behavior. The CLI supplies its existing user sign-in provider as that fallback. Automatic discovery of the saved CLI session in the Rust SDK is in development. For now, use a fixed token or supply an token_source.

Use the embedded backend

The embedded backend is how applications read and write Tables today (see Choose a backend). Enable the embedded feature, create ScanDAL with the storage transports it may use, and give it to the builder:

let config = ScandalConfig {
    spfs_endpoint: Some(spfs_url.parse()?),
    local_files: false,
    // Storage the Tables API grants with a provider credential is read without SPFS.
    provider_roots: true,
    refuse_custom_endpoints: false,
};
let client = Client::builder()
    .api_url("https://api.spiraldb.dev")
    .bearer_token(token)
    .embedded_scandal(config)
    .build()?;

The embedded feature links the native archive for your SDK release and target into your application. The binary carries ScanDAL and needs no library at run time. The SDK uses nightly Rust features.

Its registry package includes the native archive checksums and a setup script. The script downloads the archive from downloads.spiraldb.com, checks its checksum, and caches it for the build. Public downloads require no GitHub credentials. Cargo does not download the archive itself. A build without the matching archive prints the required setup command.

Install spiral-sdk from crates.io. Run scripts/setup-native-runtime.py from the unpacked SDK crate before the build. To use an archive from another directory, set SPIRAL_NATIVE_DIR.

For Tables in Spiral-managed storage, point ScanDAL at SPFS, Spiral's storage signing endpoint, with ScandalConfig::spfs_endpoint. For each Table operation, the SDK fetches short-lived storage access that covers only that Table, so your process never holds long-lived bucket keys. SPFS signs the requests; the bytes travel directly between your process and the bucket. Because of that signing step, SPFS must be reachable while you read or write. Storage access names the SPFS endpoint that signs it, and ScanDAL signs there; spfs_endpoint signs access that names none. The SDK requests storage access again before it expires, so the stack checks your permission on the Table at each renewal.

Set ScandalConfig::provider_roots to let ScanDAL read storage that the Tables API grants with a short-lived provider credential, an S3 session or a Google Cloud Storage token, instead of SPFS signing. The requests then go straight to the bucket's endpoint. With it set, ScanDAL also reads read-only storage access that carries a provider credential beside its SPFS signing directly at the bucket. Set ScandalConfig::refuse_custom_endpoints as well to accept only AWS and Google Cloud Storage sessions, refusing an S3 session that names its own endpoint, and storage access signed at an SPFS endpoint other than spfs_endpoint; hosted ScanDAL does.

Set ScandalConfig::local_files to read and write file:// Tables; it needs no storage endpoint.

When an Application authenticates as an installation, the installation retains the selected ScanDAL. The SDK obtains Table storage access using the installation's credentials.

Share read budgets across embedded clients

Reuse the same client and NativeScandal across requests. The client reuses each Table's read access until five minutes before it expires, and the runtime keeps the metadata of each exact version it read with that access, so reading the version again makes no setup requests while that metadata is kept. Without a shared read cache (below), rows and files are not kept: each read fetches them from storage. Version metadata is kept for at most 15 minutes, shortened to the credential's expiry, within a 64 MiB budget that charges at least 512 KiB per version. Eviction runs asynchronously; active reads keep their own references. A grant revoked meanwhile stops new reads once the client renews that access.

Create a NativeReadCache with ReadCacheOptions and pass it to NativeScandal::with_read_cache when independent embedded clients should share one memory and I/O budget. Its default budget retains up to 128 MiB of encoded pages, coalesces concurrent misses, and bounds speculative read-ahead. Decoded batches, active I/O, and caller-owned buffers are outside that retention budget. cache.stats() reports retained bytes and cumulative requested I/O.

Read stored positions in an exact version

Set ScanOptions::row_range to Some(ScanRowRange { start, end }) to read a half-open interval of stored positions. Positions concatenate fragments before filtering and removing deleted rows, so the result may contain fewer rows than the interval's length. A row range uses one ordered stream and cannot accompany a split token. Save the exact version with an interval: positions change across rewrites and compaction.

Run a reader on GCP

A deployed application can use its GCP host identity instead of a fixed token. Enable gcp and embedded; the GCP feature includes workload-auth for shared caching and refresh. It does not enable the AWS providers or interactive user sign-in. Other platforms select aws, github, kubernetes or modal. Custom OIDC assertions use workload-auth. Client secrets and private keys remain available without a cloud feature.

Register a Workload and configure its platform identity and read permissions first. Set SPIRAL_WORKLOAD_ID, SPIRAL_AUTH_URL, SPIRAL_API_URL and SPIRAL_SPFS_URL in your deployment. The authorization endpoint issues tokens; the API endpoint is the resource those tokens access. The host identity must match the Workload registration.

Build the client inside an active Tokio runtime:

let source = GcpAuthProvider::new(token_endpoint(&Url::parse(auth_url)?)?, workload_id)
    .with_resource(api_url)
    .into();
let credentials = RefreshingAuthTokenProvider::new(source).into();
let config = ScandalConfig {
    spfs_endpoint: Some(spfs_url.parse()?),
    local_files: false,
    provider_roots: true,
    refuse_custom_endpoints: false,
};
let scandal = NativeScandal::new(&config)?;
let client = Client::builder()
    .api_url(api_url)
    .token_source(credentials)
    .scandal(Arc::new(scandal))
    .build()?;

The refreshing provider shares token acquisition across concurrent requests. It renews early in the background and also checks the usable lifetime when a request asks for a token, so a paused container can refresh when it resumes. Missing host identity or a failed token exchange returns an error; it does not open a browser for sign-in.

Read a bounded sample with the same client:

let tables = client.project(project_id)?.table_session("main").await?;
let table = tables.table(table_path).await?;
let mut batches = table
    .scan(ScanOptions {
        limit: Some(10),
        ..Default::default()
    })
    .await?
    .into_stream();
let mut rows = 0;
while let Some(batch) = batches.next_batch().await {
    rows += batch?.num_rows();
}

The snippets use the Project, Table, and environment settings described above. The application needs the native archive for its deployment target. GCP identity selection does not grant access by itself.

The basic spiral::auth module is available without workload features. A TokenSource wraps an async closure, so an application supplies its own credentials with TokenSource::new, or a fixed token with TokenSource::fixed. Every provider in spiral::auth converts into a TokenSource with into().

Resource clients

The client also hands out typed clients for the Spiral services:

MethodService
projects()Projects and access grants
organization()Organizations, members, invitations and settings
workloads()Workloads, public keys and trust policies
applications()Applications, scopes and sign-in policy
installations()Application installations
catalog()Catalog
tables()Tables
connections()Connections
locations()Locations
tasks()Tasks
vector_indexes()Vector Indexes

For reading and writing rows, open a Table session rather than using tables(). The session resolves names, pins versions, and fetches storage access for you.

Use projects() to list or create Projects; use project(id).delete() to delete one. project(id) selects a Project for resource reads and commits without fetching its metadata. Resource handles retain the connection and identity used to select them. Use application(id), installation(id) and workload(id) for operations on a specific resource. applications().current() selects the authenticated App; installations().current() selects the authenticated installation. installations().for_user().await? discovers a delegated user's installation, returning an optional UserInstallation view. No new token is issued. The view exposes read-only discovery; it has no installation-administration methods. Its projects() method returns Project handles that retain the user credential. Every project and resource read is checked against the live user∩installation intersection by the server. info() reads metadata returned by discovery without another request; get() refreshes it.

Collection methods return one List builder. Set page_size and optionally page_token, then call send().await for a Page or stream() for lazy item iteration. Both retain query filters and resolve credentials for every request. Constructing the builder performs no I/O. Dropping the stream stops pagination. Errors use spiral::Error; a failed page ends the stream without skipping data.

use futures::TryStreamExt;

let project = client.project("robotics")?;
let metadata = project.get().await?;
let grants = project.grants(None).stream().try_collect::<Vec<_>>().await?;

let app = client.applications().current();
let page = app.installations().page_size(50).send().await?;
let mut installations = app.installations().stream();
while let Some(installation) = installations.try_next().await? {
    println!("{}", installation.id());
}

get() reads current metadata; selecting a handle does not cache authorization. An App token discovers installations but does not grant access to their data. Use installation credentials for background project access, or the delegated user installation handle for interactive access.

Authenticate as an installation

Authenticate the Application once, then obtain a handle with renewable installation credentials. ApplicationInstallation exposes get() and authenticate(); it has no project or administration operations before authentication.

let client = spiral::Client::builder()
    .api_url(api_url)
    .auth_url(auth_url)
    .private_key(application_id, private_jwk)?
    .build()?;
let app = client.applications().current();
let installation = app.installation(installation_id).authenticate().await?;
let main = installation.project(project_id)?.branch("main").await?;

For a trusted external issuer, replace private_key(...) with trusted_issuer(application_id, assertion_provider). The provider supplies fresh platform assertions. Both paths renew app and installation credentials.

ApplicationRegistration and InstallationRegistration, returned by client.application(id) and client.installation(id), hold management operations. App-authenticated and user-delegated handles do not expose those operations. An authenticated Installation provides project access with installation credentials; UserInstallation provides it with delegated user credentials. Neither changes into the other implicitly. Newly authenticated installation handles use hosted execution; projects selected through an existing installation/user connection preserve its backend.

Choose where Table operations run

Use embedded_scandal with the SDK's embedded feature to read and write Tables in your application process. It reads the spiral:// addresses that rows name, such as a file under a Location, with the client's own identity, and each identity the client acts as gets its own. Prepare the matching native library as described in Use the embedded backend. Use scandal_url to send the same operations to a remote ScanDAL service.

The SDK resolves each Table's storage access with the client's own identity and reuses it for that client's later reads, so a token provider must keep renewing one identity; build another client to act as another. An installation authenticated later supplies its own storage access, and a delegated user client keeps the user's intersected access. With scandal_url, the SDK also narrows the client's access token to the capability that a hosted ScanDAL service requires, and narrows it again before it expires or when the client's token changes; an installation narrows its own. Select one ScanDAL per client: scandal and scandal_url cannot be combined. To test or wrap execution, implement the Scandal trait with the types in spiral::scandal and pass it to scandal.

Both ScanDAL selections support Table::write_blob for a bounded byte stream, Table::read_blob for full or range reads, and Table::stat_blob for the stored size and object version without reading bytes. Uploading returns a Table-owned Blob; append its Blob::array representation using Blob::field and publish the evidence row in a commit with the other changes. Uploading alone does not publish a row. A full read validates the content digest.

For payloads too large for one request, such as video, begin the write with Table::writer and upload into it with TableWrite::upload_blob, which sends the payload in parts and retries each part on its own. See Upload large blobs with their rows. To retry a commit after a restart with the same operation, save the frozen commit in durable application state before publishing. See Survive a process restart.

Found a problem? Edit this page or report an issue.

On this page