rp-supabase-storage
Async Rust client for the Supabase Storage API. It manages buckets, uploads and downloads objects, and creates signed and public URLs.
Install
[]
= "0.3"
= { = "1", = ["macros", "rt-multi-thread"] }
Quickstart
use Url;
use ;
# async
Use StorageClient::new_with_client to reuse an existing reqwest::Client and its connection pool.
The default client from StorageClient::new follows redirects (up to 10) only to the same origin, so
the API key never goes to another host; a client you pass in is used unchanged.
Auth model
Every request sends the apikey header. The Authorization header depends on the key and token:
StorageClient::newwith a legacy JWT key (anon or service role) also sendsAuthorization: Bearer <api_key>. With the service role key, the client bypasses row level security (RLS). Use it only on a server.StorageClient::newwith a new-format key (sb_publishable_...orsb_secret_...) sends noAuthorizationheader. These keys are not JWTs; the gateway checks theapikeyheader.with_access_token(user_jwt)returns a client that sends the user's JWT as the bearer token. Storage then applies the RLS policies onstorage.objectsandstorage.bucketsfor that user. Use the anon key plus a user token in code that acts for a user.
# use ;
#
public_url sends no request. The URL works only when the bucket is public. For private
buckets, call create_signed_url or create_signed_urls. Both use the batch signing endpoint,
because the single-object endpoint returns an invalid signature for keys with ?. The API
returns signed URLs relative to /storage/v1 with the object key unescaped; this crate rebuilds
each URL from the bucket and the encoded object path, then appends the token. Keys with ? and
spaces work. The Storage server itself rejects some characters in keys, for example #.
BucketOptions fields are always sent. On update_bucket, None removes an existing file size
limit or MIME type list. FileOptions::default() sends Cache-Control: max-age=3600.
Object paths
Separate path segments with /, for example users/1.png. The client percent-encodes each
segment once. It rejects empty paths, empty segments (a//b, leading or trailing /), . and
.., tabs, carriage returns, and line feeds with StorageError::InvalidPath before it sends a
request.
Errors
All operations return StorageError:
Api { status, body }: the server answered with a non-success status.bodyisApiErrorBody::Storagewith optionalstatus_code,code,error, andmessagewhen the server sent the standard Storage error JSON, elseApiErrorBody::Rawwith the body text.InvalidPath: a bucket id or object path is not valid.MissingSignedToken: a signed URL from the server has no?token=query.SignFailed { path, message }:create_signed_urlgot no URL for the object, for example because it does not exist or the caller cannot read it.Http,Json,Url,InvalidHeader,InvalidBaseUrl: transport, decoding, and input errors.
# use ;
# async
StorageError::code() returns the body's machine-readable code as a StorageErrorCode. A code
this crate does not know is StorageErrorCode::Other.
Most object errors arrive as HTTP 400 with the real status in the body's statusCode.
StorageError::api_status() returns that status, or the HTTP status when the body has none.
Bucket::remove is the bulk endpoint. It skips missing paths and paths that row level security
hides, without an error. Bucket::remove_object deletes one object and fails when nothing is
deleted: api_status() is 404 and code() is NoSuchKey for a missing object, and
api_status() is 403 and code() is AccessDenied for a denied delete.
Bucket::download reads the whole body into memory. Bucket::download_stream returns after the
headers arrive. Read the body with chunk() or into_stream(), and stop when the byte count
passes your limit:
# use ;
# async
Images and resumable uploads
TransformOptions supports width/height, ResizeMode::{Cover, Contain, Fill}, quality and
ImageFormat::Origin. Use public_transformed_url, download_public_transformed,
download_transformed (authenticated streaming), or create_signed_transformed_url.
Image transformation must be enabled on the server. Signing submits the options in the
transform body and reconstructs the trusted local render/image/sign URL from the returned
token. Ordinary signing retains the batch workaround for object names containing ?.
Signed transformations validate the returned JWT's typed url claim against the exact
requested bucket/key before returning a URL (this is not cryptographic verification).
Known upstream limitation: the single-object signing route uses JavaScript decodeURI,
which preserves reserved escapes such as %3F; a question-mark key can therefore produce
a claim that the image renderer rejects with InvalidSignature. The client reports
StorageError::Protocol with remote signed-path mismatch instead of issuing an unusable
URL. It cannot repair a signed claim without the server's signing key, and never uploads
a transformed replacement object. See upstream
getSignedURL.ts
and signObjectUrl.
create_resumable_upload(path, length, options) creates a TUS 1.0 session.
Persist its sensitive url() and the creation option upsert; reopen it using
StorageClient::resume_upload(&url, upsert). The overwrite intent is sent on POST, HEAD,
and every PATCH, including after reopening.
Creation completes a zero-byte object; an empty chunk on a confirmed complete upload is a local no-op.
write_chunk(offset, Bytes) accepts exactly 6 MiB except the final tail, including zero bytes
for an empty object. No full-file buffering is required. Reopening performs HEAD; conflicts
and mismatched acknowledgements reconcile with HEAD. A failed or cancelled PATCH leaves
the session dirty, and the next write consults HEAD before accepting bytes. Supply bytes
from the refreshed offset; stale offsets are rejected rather than silently replayed.
Expired uploads return the server's 404/410 error and must be recreated explicitly.
with_retry_policy configures shared 429 handling and survives token/project clones.
PATCH is always sent once, irrespective of mutation-retry opt-in.
Default clients follow redirects only within the same origin. A supplied HTTP client retains
its configuration; callers must configure no redirects or same-origin-only redirects,
because arbitrary redirect policies can forward the custom apikey header.
Protocol references: Supabase image transformations, Supabase resumable uploads, TUS 1.0 protocol.
The ignored live_transforms_and_resumable_roundtrip test requires SUPABASE_URL and
SUPABASE_SERVICE_ROLE_KEY, an image-enabled local server, and permission to create buckets.
It checks exact 64×64-to-16×16 PNG rendering and resumed upload/overwrite larger than 6 MiB
byte-for-byte. regression_live_questionmark_signed_transform_reports_remote_mismatch
checks the explicit upstream signing error for question-mark keys.
Limits
- No S3 protocol support, analytics buckets, or vector buckets.