Manage a Connection
Test a Connection, rotate its credential, allow writes, change its description, and drop it.
Keep a Connection working when keys rotate, and cut off access when you must. Updating the credential or dropping the Connection is how you withdraw the access that a Connection delegates. Neither changes the Locations that name it. Readers keep any short-lived credential that they already hold for up to one hour.
For Rust, pin a branch with project.branch("main") and resolve the
Connection with head.connection(path). Use head.connections() to list names
and metadata. Paths use / between segments; the CLI also accepts dotted
names. The pinned head fixes the name binding. Credential and description
reads return the current values, including when you select an older commit.
Create and drop through a Commit on main.
Credential, description and allowed-access updates call client.connections()
directly and publish no Catalog commit.
These operations also work with the SDK's tables feature disabled.
Before you start
connection:updatefor a test or an update, andconnection:dropfor a drop.- For HTTP, the Connection ID and an access token in
$SPIRAL_TOKEN. The commandspiral connections --project demo show robotics-bucket --jsonprints the ID.
Check that a Connection works
head = client.project("demo").branch("main")
found = client.catalog.resolve_path("demo", head.commit_id, CatalogResolvePathRequest(path=[name]))
if found.resource is None:
raise LookupError(f"no Connection named {name}")
result = client.connections.test_connection(found.resource.descriptor.resource_id)
print(f"credential accepted: {result.vend.succeeded}")
if result.access is not None:
print(f"storage reachable: {result.access.succeeded}")The test reports one result for each step:
vend: Spiral obtains a short-lived credential from the stored credential, or from the Connection's identity foroidc. Forhugging_face, Spiral first asks the Hub to accept the stored token, because the Hub gives a short-lived token for a public repository with any token. Forr2, Spiral first lists with the stored key, because it signs the short-lived credential itself and R2 checks the key only at a read.access: Spiral lists at most one page of files under the Connection URI with the short-lived credential. Forhugging_face, it lists themainbranch. This step is absent when step 1 fails.write: for a Connection that allowswrite, Spiral starts and aborts an upload under the Connection URI, as raising it does. This step is absent for a read-only Connection and when step 2 fails.
A key or a role that the provider refuses fails at step 1. A missing bucket or a key without read access fails at step 2. The test returns no credential, and Spiral discards the short-lived credential.
Readers of an r2 Connection learn of a revoked key later than
the test does. Spiral signs the short-lived credential itself, so a key that
Cloudflare has revoked still produces one, and the reader fails at its first
read because R2 cannot verify the token. Update the credential with the new
key, or drop the Connection.
Step 2 runs on the Spiral server. It also fails when the server cannot reach
your storage, for example when the storage accepts requests only from your
own network. Readers that can reach the storage still read through the
Connection. The detail of step 2 tells which cause applies.
The test does not check KMS access. Read an encrypted object to check the IAM and key policies.
Reach your storage from hosted Spiral
For a MinIO, RustFS, Ceph, or self-hosted Hugging Face Connection, provide a public HTTPS endpoint. Hosted Spiral checks the destination on each credential request and access test. It refuses private, loopback, link-local and other special-use addresses, including when a hostname resolves to one, and follows no redirects. Credential and probe responses have a five-second deadline and a 64 KiB size limit.
A failed step reports unreachable, refused, or unauthorized; it does not
return the endpoint's response text. If your storage is private-only, arrange
public access for its credential service before using it from hosted Spiral.
A local development stack permits local and LAN endpoints, and readers on
your own machine retain their local network access.
Update a credential
An update replaces the whole credential. Give every field again, with the secret for a type that holds one, even to change only an endpoint or a region. A field that you leave out is cleared. Spiral therefore never uses a stored secret with an endpoint or an access key ID that it was not stored with.
To rotate an AWS key, create a new access key for the IAM user, then send the new ID and secret in one update:
head = client.project("demo").branch("main")
found = client.catalog.resolve_path("demo", head.commit_id, CatalogResolvePathRequest(path=["robotics-bucket"]))
if found.resource is None:
raise LookupError("no Connection named robotics-bucket")
secret = Path("new-aws-secret-access-key.txt").read_text().rstrip("\n")
credential = ConnectionsCredential(
aws_access_key=ConnectionsAwsAccessKey(
access_key_id="AKIAI44QH8DHBEXAMPLE",
secret_access_key=secret,
region="us-east-1",
)
)
client.connections.update_credential(
found.resource.descriptor.resource_id, ConnectionsUpdateCredentialRequest(credential=credential)
)The replacement credential must use the same provider as the Connection. The HTTP
API names both the provider and authentication method in one union arm, such as
awsAccessKey, awsOidc, or huggingFaceToken. Its fields sit inside the matching
payload object.
The authentication method can change within the provider. An
update moves an AWS or Ceph Connection between access_key and oidc,
because the provider is the same for both. A Google Cloud Storage Connection
moves between service_account_key and oidc in the same way. To move from
a key to a role, send the role and no secret:
head = client.project("demo").branch("main")
found = client.catalog.resolve_path("demo", head.commit_id, CatalogResolvePathRequest(path=["robotics-bucket"]))
if found.resource is None:
raise LookupError("no Connection named robotics-bucket")
credential = ConnectionsCredential(
aws_oidc=ConnectionsAwsOidc(role_arn="arn:aws:iam::123456789012:role/spiraldb-reader", region="us-east-1")
)
client.connections.update_credential(
found.resource.descriptor.resource_id, ConnectionsUpdateCredentialRequest(credential=credential)
)The update deletes the secret of the key. The role must trust the Connection before its next read. See Connect with an IAM role.
An update deletes the secret that it replaces. It publishes no Catalog commit, and it does not check the new credential. Run the test after it. Credentials that Spiral already gave out stay valid until they expire, so an update does not switch every reader at once.
Allow writes
Every Connection starts read-only. Raise its allowed access to write when
you want Spiral to export results into the bucket:
| Allowed access | What resources may do under the Connection URI | Use it for |
|---|---|---|
read | Get and list objects | Locations, and every Connection by default |
write | Also create objects in a directory that Spiral allocated, never replace or delete one | Exports, including to append-only or object-locked buckets |
No level replaces or deletes an object. A Connection reaches data that you manage, so Spiral reads it in place and writes exports only as new objects in directories that it allocates. Tables that Spiral manages, which compact, collect garbage and purge dropped data, are never stored in a Connection.
head = client.project("demo").branch("main")
found = client.catalog.resolve_path("demo", head.commit_id, CatalogResolvePathRequest(path=["robotics-bucket"]))
if found.resource is None:
raise LookupError("no Connection named robotics-bucket")
client.connections.update_allowed_access(
found.resource.descriptor.resource_id, ConnectionsUpdateAllowedAccessRequest(allowed_access="write")
)Spiral first exercises write with a short-lived credential: it starts and
aborts an upload under .spiraldb-write-check/, which leaves no object behind.
If the provider refuses, the change fails and the Connection keeps its level.
Grant the stored key or role that upload first, and no more: see
Amazon S3
and Google Cloud Storage.
Only aws and gcs Connections can allow write. The other providers allow
only read:
| Provider | Why it allows only read |
|---|---|
r2 | R2's only scope that writes also replaces and deletes objects. |
minio, rustfs | The server refuses the policy condition that stops a credential from replacing objects. |
ceph | The gateway does not enforce that policy condition. |
hugging_face | A Hub repository is not an object store. |
Each resource then receives credentials for its own directory only, and they can create objects but not replace or delete them. Credentials that you vend directly, and every Location, stay read-only.
Set read to withdraw the approval. Resources then receive read-only
credentials from their next request, and write credentials that Spiral
already gave out expire within an hour. Exports into a Connection are
planned; until then the allowed access has no effect on reads.
Change the description
head = client.project("demo").branch("main")
found = client.catalog.resolve_path("demo", head.commit_id, CatalogResolvePathRequest(path=["robotics-bucket"]))
if found.resource is None:
raise LookupError("no Connection named robotics-bucket")
client.connections.update_description(
found.resource.descriptor.resource_id,
ConnectionsUpdateDescriptionRequest(description="Robot recordings, read-only, 2026 onward"),
)Send "description": null to clear it. The Connection page in Studio also
changes the description.
Drop a Connection
Drop a Connection on the branch main. The drop retires the name. Spiral
checks for dropped Connections every minute and deletes their secrets:
commit = client.project("demo").branch("main").commit()
commit.drop_connection("robotics-bucket")
try:
head = commit.publish()
except (spiral.BranchMoved, spiral.PublishConflict) as error:
raise RuntimeError("Project changed; reload before retrying") from error
print(f"dropped at commit {head.commit_id}")A Location that names a dropped Connection then fails at read time with an error that names the Connection, who dropped it, and when. This is also true on a branch that still binds the name. The record stays readable as metadata, so an earlier Project commit still resolves.
Credentials that Spiral already gave out stay valid until they expire. This is why their lifetime is one hour.
Change the URI or the provider
The URI and the provider cannot change. Drop the Connection and create a new one. The new Connection can have the same name:
project = client.project("demo")
dropping = project.branch("main").commit()
dropping.drop_connection("robotics-bucket")
dropping.publish()
creating = project.branch("main").commit()
credential = ConnectionsCredential(
aws_access_key=ConnectionsAwsAccessKey(
access_key_id="AKIAIOSFODNN7EXAMPLE",
secret_access_key=Path("aws-secret-access-key.txt").read_text().strip(),
)
)
creating.create_connection("robotics-bucket", "s3://example-robotics-2026/", credential)
creating.publish()Between the two commands, no Connection has the name robotics-bucket.
Locations keep the Resource ID of the dropped Connection. They fail until a new Location version names the new Connection. See Change the root. This is deliberate: the inventory of such a Location records URIs under the dropped Connection, and the new Connection does not necessarily reach them.