# Storage Capabilities
Rindle exposes storage capabilities as an explicit adapter contract so adopters
can tell which upload and delivery flows are expected to work before wiring a
profile to a backend. Unsupported flows fail with tagged tuples instead of
falling back silently.
This guide is the canonical capability reference for v1.1. Other guides link
here instead of repeating provider matrices inline.
## Shipped Capability Vocabulary
Rindle currently knows these capability atoms:
| Capability | Meaning today |
| ---------- | ------------- |
| `:presigned_put` | The adapter can mint a direct-upload URL for a single-object PUT. |
| `:multipart_upload` | The adapter can initiate, sign parts for, complete, and abort an S3-style multipart upload. |
| `:signed_url` | The adapter can mint a time-limited signed delivery URL for private delivery. |
| `:head` | The adapter can read remote object metadata for verification and cleanup. |
| `:local` | The adapter is backed by local filesystem semantics. |
| `:resumable_upload` | The adapter can drive a resumable upload lifecycle instead of a single-shot direct PUT. |
| `:resumable_upload_session` | The adapter can create and continue provider-owned resumable upload sessions. |
These atoms are shipped vocabulary, but adapter-specific. Rindle does not imply
that every storage backend supports them.
## Unsupported Flow Contract
Rindle uses tagged unsupported tuples when a flow requires a capability the
adapter does not advertise:
| Flow family | Tagged error |
| ----------- | ------------ |
| Upload | `{:error, {:upload_unsupported, capability}}` |
| Delivery | `{:error, {:delivery_unsupported, capability}}` |
Examples:
- `Rindle.Storage.Local` returns `{:error, {:upload_unsupported, :multipart_upload}}`
for multipart entrypoints.
- Private delivery against an adapter without `:signed_url` returns
`{:error, {:delivery_unsupported, :signed_url}}`.
- A resumable path against an adapter that does not advertise it remains
intentionally explicit:
`{:error, {:upload_unsupported, :resumable_upload}}`.
These failures are part of the adopter-facing contract. Rindle does not guess,
downgrade, or silently swap in another flow.
## Provider Matrix
The table below describes the current v1.1 posture for adapters and common
provider choices.
| Backend / provider | Runtime seam | Expected capabilities today | Proof posture | Notes |
| ------------------ | ------------ | --------------------------- | ------------- | ----- |
| Local filesystem | `Rindle.Storage.Local` | `[:local, :presigned_put]` | Automated in the default test suite | Presigned PUT is a local-development parity shim, not a remote-object-store claim. `Rindle.Storage.Local` does not advertise `:resumable_upload` or `:resumable_upload_session`. |
| MinIO | `Rindle.Storage.S3` | `[:presigned_put, :head, :signed_url, :multipart_upload]` | Automated in default CI and local integration lanes | This is the always-on real S3-compatible proof for direct PUT, multipart upload, metadata verification, and signed delivery URL generation. `Rindle.Storage.S3` does not advertise the resumable capability family. |
| Generic S3-compatible provider | `Rindle.Storage.S3` | `[:presigned_put, :head, :signed_url, :multipart_upload]` | Expected by contract; not proven against every vendor in default CI | Rindle uses the shipped S3 adapter seam. Provider-specific behavior beyond that seam should be validated in adopter-owned environments. |
| Cloudflare R2 | `Rindle.Storage.S3` | `[:presigned_put, :head, :signed_url, :multipart_upload]` when the provider honors the shipped S3-compatible operations | Documented compatibility target; adopters validate vendor behavior in their own environments | Phase 8 does not add a bespoke R2 adapter. The repo only claims the current shipped S3-style operations it can exercise through the existing adapter seam, with MinIO as the automated proof lane. |
| Google Cloud Storage | `Rindle.Storage.GCS` | `[:head, :signed_url, :resumable_upload, :resumable_upload_session]` | Live GCS proof exists in the GCS test lanes; adopters still own bucket and browser wiring | `Rindle.Storage.GCS` is the shipped adapter that honestly advertises the resumable capability family. See [`storage_gcs.md`](storage_gcs.md) for runtime wiring, CORS, and session hygiene. |
## Proof Boundaries
Rindle separates "documented contract" from "what the repo proves by default":
- MinIO is the default real-provider proof lane in CI for the shipped S3
adapter contract.
- Cloudflare R2 is documented as a compatibility target through the shipped
`Rindle.Storage.S3` seam.
- Default CI proves the shipped S3-style contract against MinIO, not against
every vendor-branded backend.
- Generic S3 providers are expected to match the shipped S3 adapter contract,
but adopters should still validate vendor-specific behavior in their own
environments before rollout.
That distinction matters: Phase 8 improves auditability, not marketing claims.
This guide does not imply provider-specific live R2 proof in CI.
## Adapter Honesty
Capability claims are adapter-specific, not marketing-wide:
- `Rindle.Storage.GCS` advertises `:resumable_upload` and `:resumable_upload_session`
- `Rindle.Storage.S3` advertises neither resumable capability today
- `Rindle.Storage.Local` advertises neither resumable capability today
- custom adapters may honestly advertise either, both, or neither depending on
what they actually implement
Rindle does not silently downgrade resumable requests into presigned PUT.
## Cloudflare R2 Boundary
Cloudflare R2 is documented here as an S3-compatible provider path through the
existing `Rindle.Storage.S3` adapter. In v1.1, the supported Rindle contract is:
- Direct upload via presigned PUT.
- Metadata verification via `head/2`.
- Signed delivery URL generation when `:signed_url` is advertised.
- S3-style multipart upload when `:multipart_upload` is advertised.
This guide does not claim:
- A bespoke `Rindle.Storage.R2` adapter.
- HTML form POST uploads as part of the shipped contract.
- Provider-specific live R2 coverage in CI.
- A shipped resumable-upload API through the S3 adapter.
## Resumable Boundary
Resumable upload is shipped where the adapter advertises it. Today that means
`Rindle.Storage.GCS`.
That does not mean:
- every adapter supports resumable upload
- Rindle falls back automatically from resumable upload to presigned PUT
- S3-compatible providers inherit resumable semantics through `Rindle.Storage.S3`
- Rindle ships tus or a provider-agnostic resumable abstraction beyond the
honest capability contract