Client
Configure the Rust SDK client with an endpoint, a credential, and the embedded backend.
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 method | Sets |
|---|---|
api_url | The Spiral API endpoint. Required. |
bearer_token | A fixed bearer token. |
token_source | A provider that the client calls for a token before every request. |
http_client | A reqwest::Client, to share one connection pool with the rest of your application. |
embedded_scandal | Run 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. |
scandal | Where Table operations run: a ScanDAL you construct, such as a NativeScandal. |
scandal_url | The 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:
SPIRAL_TOKEN.- With
private-key-auth, a default feature,SPIRAL_CLIENT_IDandSPIRAL_PRIVATE_KEY. SPIRAL_CLIENT_IDandSPIRAL_CLIENT_SECRETfor an older Workload.- With
workload-auth,SPIRAL_WORKLOAD_IDand a custom OIDC assertion or an enabled platform identity from Kubernetes, Modal, GitHub Actions, GCP or AWS. - 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:
| Method | Service |
|---|---|
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.