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:
| Method | Purpose |
|---|---|
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 completionPause / 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.