🎉 Native Google & Apple sign-in is here → read the guide
Storage

Storage

Supabase Storage keeps your files — images, videos, PDFs, anything — in named containers called buckets. Think of a bucket as a top-level folder: a public bucket is readable by anyone who has the URL (good for avatars or product photos), while a private bucket hands out files only through a temporary, signed link. The supabase-storage module lets you create buckets, upload and download files, list them, and generate those shareable links — plus resize images on the fly.

Like everywhere else in the library, each call returns a SupabaseResult you handle with onSuccess / onFailure — see Results & Errors if that’s new. Start by building a storage client from your SupabaseClient:

val storage = createStorageClient(client)

Buckets

A bucket is the container your files live in, and you create it once before uploading anything. Setting public = true makes its files readable by URL with no expiry; public = false keeps them private (you’ll hand out signed links instead). emptyBucket clears the files but keeps the bucket; deleteBucket removes the bucket itself.

storage.createBucket(id = "avatars", name = "avatars", public = true)
storage.getBucket("avatars")
storage.listBuckets()
storage.updateBucket(id = "avatars", public = false)
storage.emptyBucket("avatars")
storage.deleteBucket("avatars")

Upload & download

To store a file you give it a path inside the bucket (the part after the bucket name, like a sub-folder plus filename) and the raw data as a ByteArray. The contentType tells browsers how to render it. upsert = false means “fail if a file already exists at this path”; set it to true to overwrite instead (upsert = update-or-insert). Downloading does the reverse: hand it the same bucket and path and get the file contents back.

storage.upload(
    bucket = "avatars",
    path = "user-123/photo.png",
    data = bytes,                       // ByteArray
    contentType = "image/png",
    upsert = false,
)
 
val contents: SupabaseResult<String> = storage.download("avatars", "user-123/photo.png")

The full set of file operations:

MethodPurpose
upload(bucket, path, data, contentType, upsert, cacheControl?)create a file
update(bucket, path, data, …)overwrite an existing file
download(bucket, path)fetch the file contents
move(bucket, fromPath, toPath) / copy(bucket, fromPath, toPath)relocate / duplicate
remove(bucket, paths) / removeWithResult(…)delete one or many
info(bucket, path) / exists(bucket, path)metadata / existence

Resumable uploads

A plain upload sends the whole file in one request, so a dropped connection means starting over. For large files or flaky networks, use a resumable (also called TUS, after the protocol it speaks) upload instead: the file goes up in chunks and the server remembers how far it got, so an interrupted transfer picks up where it stopped rather than restarting.

The simplest form suspends until the upload finishes — same idea as upload, just chunked and resumable under the hood:

// One-shot — suspends until done:
storage.uploadResumable(
    bucket = "videos",
    path = "user-123/clip.mp4",
    data = bytes,
    contentType = "video/mp4",
)

If you want a progress bar or pause/resume control, create the upload handle yourself instead. The handle exposes a progress flow you can collect (a value from 0f to 1f), and you start the transfer by launching await() in a coroutine. scope below is any CoroutineScope you own (a viewModelScope, a screen-scoped scope, etc.); progress.collect { } and await() are suspend, so they must run inside one:

val upload = storage.createResumableUpload(
    bucket = "videos",
    path = "user-123/clip.mp4",
    data = bytes,
    contentType = "video/mp4",
)
 
scope.launch {
    upload.progress.collect { p -> render(p.fraction) }   // 0f..1f
}
 
val job = scope.launch { upload.await() }   // runs to completion

Pause / resume across restarts. Cancelling the coroutine running await() pauses the upload, leaving uploaded bytes on the server. Persist upload.uploadUrl; later, recreate the handle with createResumableUpload(…, uploadUrl = saved) and call await() again — it HEADs the server for the current offset and continues. Chunks default to 6 MiB (RESUMABLE_DEFAULT_CHUNK_SIZE = 6 × 1024 × 1024 bytes), the size Supabase’s resumable endpoint requires.

Listing

To browse what’s in a bucket, list returns the files under a prefix (think of it as the folder path to look inside). Use limit and offset to page through large buckets, and sortBy / sortOrder to control the order.

val files: SupabaseResult<List<FileObject>> = storage.list(
    bucket = "avatars",
    prefix = "user-123/",
    limit = 100,
    offset = 0,
    sortBy = "name",
    sortOrder = SortOrder.ASC,
)

URLs

How you share a file depends on its bucket. For a public bucket, getPublicUrl returns a permanent link anyone can open — it never expires. For a private bucket, you instead mint a signed URL: a temporary link that stops working after expiresIn seconds, so you can grant access without making the file public. There’s a batch version for many files at once, and a pre-signed upload token so a browser or untrusted client can upload directly without your keys.

// Public bucket — no expiry:
val url = storage.getPublicUrl("avatars", "user-123/photo.png")
 
// Private bucket — time-limited:
val signed = storage.createSignedUrl(
    bucket = "avatars",
    path = "user-123/photo.png",
    expiresIn = 3600,           // seconds
    download = false,
)
 
// Many at once:
storage.createSignedUrls("avatars", listOf("a.png", "b.png"), expiresIn = 3600)
 
// Browser-side uploads (pre-signed token):
storage.createUploadSignedUrlWithPath("avatars", "user-123/new.png").onSuccess { signed ->
    storage.uploadToSignedUrl("avatars", "user-123/new.png", token = signed.token, data = bytes)
}

Image transforms

You don’t need to store multiple sizes of an image. Pass ImageTransformOptions to getPublicUrl or createSignedUrl and Supabase resizes and re-encodes the file on the fly — so the same original can serve a 200×200 thumbnail and a full-size view from one stored file:

storage.getPublicUrl(
    bucket = "avatars",
    path = "user-123/photo.png",
    transform = ImageTransformOptions(
        width = 200,
        height = 200,
        resize = ResizeMode.COVER,   // COVER | CONTAIN | FILL
        quality = 80,                // 20–100
    ),
)

Supabase Storage also exposes Analytics (Iceberg catalog) and S3 Vectors endpoints. Those surfaces are available on StorageClient for advanced workloads but most apps only need the bucket/file APIs above.