Connections
Connect a private bucket or a Hugging Face repository once, and let your team read through it without the key.
A Connection lets your whole team read a private bucket or a Hugging Face repository without anyone holding its keys. You let an AWS IAM role or a Google Cloud service account trust the Connection's own identity, or you store a key or a token once. When a teammate reads a Location that uses the Connection, Spiral gives them a read-only credential that reaches only that Location's root and expires within the hour. Nobody, including you, can read a stored secret back.
HTTP API
The HTTP API is available under /api/connections. Requests use camelCase fields and
tagged unions with matching discriminator and payload names. Counts and timestamps
that use 64-bit integers are returned as decimal strings. Existing Rust and CLI
clients continue to use the established endpoints.
An example
Your robot recordings sit in a private bucket. You create a Connection
robotics-bucket for s3://example-robotics/ with a read-only IAM key, and a
Location recordings for the root s3://example-robotics/recordings/ inside
it:
Connection robotics-bucket s3://example-robotics/ holds the key
└─ Location recordings s3://example-robotics/recordings/ lists the filesA teammate with read access to recordings lists its files and reads them
with their own S3 client. They need no AWS_* variables and no permission on
the Connection itself. The credential that Spiral gives them can read
recordings/ and nothing else in the bucket.
A Connection to Hugging Face works the same way. The Connection hugging-face
holds a token for hf://datasets/example-robotics/robot-runs, and a Location
over it, such as hf://datasets/example-robotics/robot-runs@main/data/, lists
one revision of that repository.
Choose a provider
A Connection's type is its provider. Each provider page covers the grants to make on your side, the Connection to create, and the test:
An IAM role that trusts the Connection, or an IAM user's access key.
A service account that the Connection impersonates through workload identity federation, or a service account key.
An R2 access key scoped to one bucket.
A role on a Ceph Object Gateway that trusts the Connection, or a user's access key that assumes the role.
A fine-grained token for one repository.
What readers receive
Spiral never hands out the stored credential. When a reader opens a Location, Spiral checks their permissions on the Location and exchanges the stored credential for a short-lived one:
- for a bucket, read and list access to the Location's root only, for one hour;
- for a Hugging Face repository, a short-lived read token for the whole repository, because the Hub cannot narrow a token further. The Location itself still lists and reads only its own root at its own revision.
The reader then reads the provider directly. Spiral signs no individual request and copies no bytes. It caches a credential for most of its life, so a burst of reads of one Location costs one exchange. Application user tokens cannot obtain cloud credentials through a Connection.
The URI sets the ceiling
A Connection names one URI: an S3 or Cloud Storage bucket, a prefix in a
bucket, or one Hugging Face repository. No credential that it issues reaches
outside that URI, whatever the stored key could do. A Location's root must lie
inside it: Spiral refuses s3://example-robotics-old/ for a Connection to
s3://example-robotics/, and checks the root again every time it issues a
credential.
Store a key, or grant a role, that can read only what the Connection names anyway. Your IAM grant is the outer limit; the Connection URI and the Location root narrow it further.
Trust one Connection, never all of Spiral
Where a provider trusts the Connection's identity, as an AWS role or a Google
Cloud service account does, Spiral's issuer https://auth.spiraldb.com signs
a short-lived token whose subject names one Connection, such as
conn_24041a03-26ef-45a2-88e2-3d89150ff6f7. Any Connection, in any
Organization, can obtain a token from the same issuer, so the subject is what
separates yours from everyone else's. Every trust policy and binding names the
subject, and the policies that Spiral prints for you already do. A grant to
the issuer as a whole, or to a whole workload identity pool, would let every
Spiral customer in.
A Connection never asks you to trust a cloud identity of Spiral's, such as a Google service account or an AWS account, because those carry no subject of yours.
What a Connection does not do
- It does not return the secret. Every read reports when the credential was last set. No operation returns the value.
- It does not have versions. The credential and the description change in place, and every read uses their current values.
- It does not branch. Only the branch
maincreates or drops a Connection, and a drop applies to every branch. A revert cannot restore a dropped Connection. - It does not time-travel. Spiral uses the Connection as it is configured today, whatever Project commit the Location was read at.
- It does not write unless you allow it, and it never deletes. Every Connection starts read-only, and every Location reads only. An administrator can allow writes so that Spiral can export into the bucket. Spiral never deletes anything through a Connection, so Tables that Spiral manages are never stored in one.
Where next
- Connect Amazon S3, Google Cloud Storage, Cloudflare R2, Ceph, or Hugging Face.
- Manage a Connection: test it, rotate its credential, allow writes, and drop it.
- Create a Location that reads through the Connection.
Reference
Providers and authentication
A Connection's type is its provider, the external system it reaches. How Spiral authenticates to the provider is a separate choice. AWS, Google Cloud Storage, and Ceph offer two; every other provider has one.
| Provider | Authentication | Spiral stores | Reads |
|---|---|---|---|
aws | oidc | The ARN of an IAM role that trusts the Connection, and an optional region | s3:// |
aws | access_key | An IAM user key, and an optional region | s3:// |
gcs | oidc | A workload identity pool provider that trusts Spiral, and the service account that the Connection impersonates | gs:// |
gcs | service_account_key | A service account key. Its private key is the secret | gs:// |
r2 | access_key | A Cloudflare account ID, an R2 access key, and an optional jurisdiction | s3:// |
ceph | oidc | A gateway endpoint, the ARN of a role that trusts the Connection, and an optional region | s3:// |
ceph | access_key | A gateway endpoint, the ARN of the role that the key assumes, a user's key, and an optional region | s3:// |
hugging_face | token | A Hugging Face token, and an optional Hub endpoint | hf:// |
minio | access_key | A MinIO endpoint, a key, and an optional region | s3:// |
rustfs | access_key | A RustFS endpoint, a key, and an optional region | s3:// |
A hugging_face Connection without an endpoint reads the public Hub,
https://huggingface.co. A minio, rustfs, or ceph endpoint, and a Hub
endpoint other than the public one, must be https:// on a host other than
loopback on a hosted deployment; local development also takes http://. An
r2 Connection names no endpoint: Spiral derives it from the account ID and
the jurisdiction, such as https://<account_id>.r2.cloudflarestorage.com. The
URI and the provider are fixed for the life of a Connection; everything else
about the credential can change with a credential update. An update also
moves an aws or ceph Connection between access_key and oidc, and a
gcs Connection between service_account_key and oidc.
An oidc credential holds no secret. The role or the service account trusts
the Connection's own identity, which Spiral proves to the provider at each
exchange, so there is no key to store and no key to rotate. Every other
authentication method holds a secret.
How the credential is exchanged
| Provider and authentication | Exchange | What the result reaches |
|---|---|---|
aws, access_key | GetFederationToken with a session policy | The objects under the root, read and list |
aws, oidc | AssumeRoleWithWebIdentity with a session policy | The objects under the root, read and list |
gcs, oidc | A Google STS token exchange, then a service account token narrowed with a Credential Access Boundary | The objects under the root, read and list |
gcs, service_account_key | A token grant signed with the stored key, then the service account token narrowed with a Credential Access Boundary | The objects under the root, read and list |
r2, access_key | A token signed with the stored key, which R2 verifies against its own copy | The objects under the root, read and list |
ceph, oidc | AssumeRoleWithWebIdentity against the endpoint, with a session policy | The objects under the root, read and list |
ceph, access_key | AssumeRole against the endpoint, with a session policy | The objects under the root, read and list |
hugging_face, token | A repository token from the Hub | The whole repository at every revision, read and list |
minio or rustfs, access_key | AssumeRole against the endpoint, with a session policy | The objects under the root, read and list |
URI rules
| Scheme | The URI names | Example |
|---|---|---|
s3:// | One bucket, or a prefix in it | s3://example-robotics/recordings/ |
gs:// | One Cloud Storage bucket, or a prefix in it | gs://example-robotics/recordings/ |
hf:// | One repository, with its type | hf://datasets/example-robotics/robot-runs |
- A prefix names a directory, so it ends with
/. - An
hf://URI names one whole repository, with its type:models,datasets, orspaces. It cannot name a directory or a revision, and a URI without a type, such ashf://example-robotics/robot-model, is refused. Hugging Face Storage Buckets (hf://buckets/...) are planned. - Connection URIs and Location roots must not reach keys that contain
*or?, or a${...}sequence, because IAM policies read these as wildcards and variables. A Connection URI is literal key text, so%2Ain it stays the three characters%2A. A Location root is a URI, so%2Ain it means*and is refused. - A Location root inside a Hugging Face repository names one revision after
the repository:
hf://datasets/example-robotics/robot-runs@curated/mcaps/readsmcaps/on the branchcurated. Write a branch name with/, such asteam/curated, asteam%2Fcurated, the way file URIs show it. A Location on a branch follows the branch; a Location at a commit lists the same files at every refresh. - Two Connections can use the same bucket and path at different S3-compatible endpoints. A reader rejects such overlapping roots while their credentials are valid, so that it never reads an object from the wrong server. For overlapping roots at the same endpoint, it uses the most specific root.
Permissions
| Operation | Permission | viewer | editor | admin |
|---|---|---|---|---|
| Read one Connection's metadata | connection:view | ✓ | ✓ | ✓ |
| Create a Connection | connection:create | ✓ | ✓ | |
| Change the description, update the credential, or test the Connection | connection:update | ✓ | ✓ | |
| Drop a Connection | connection:drop | ✓ | ✓ | |
| Name it from a Location | connection:bind | ✓ | ✓ | |
| Obtain a credential directly | connection:use | ✓ |
connection:use is limited to admin because it returns a credential with no
Location in between, so nothing records which data it was meant for. Reading a
Location that names a Connection needs no connection:* permission: the
connection:bind check when the Location was created is the delegation.
The Connections HTTP reference documents every operation.