Storage
S3-compatible object storage with CDN, image transforms, and access control
Cloud Storage is S3-compatible object storage fronted by a global CDN. Use it for user uploads, images, videos, generated assets, and any binary blob your app produces. Buckets are auto-created per project, and the supabase.storage client works the same in the browser, on the server, and inside edge functions.
Object storage
Files in Cloud Storage live in buckets and are addressable by path. Under the hood, storage is S3-compatible — but you typically interact with it through the typed supabase.storage client rather than raw S3 API calls. Files are durably replicated and served from ~50 CDN edge locations.
Buckets
Each project ships with a default public bucket and a private bucket. You can create additional buckets at any time from the Cloud tab → Storage → Buckets panel, or by asking the agent.
- Public buckets — files served directly via URL. Use for product images, avatars, OG images, marketing assets.
- Private buckets — require a signed URL or an authenticated request to download. Use for invoices, contracts, premium content, anything sensitive.
A bucket's public/private setting controls only the default read access. Fine-grained rules — "users can read their own files but not others'" — are enforced via RLS policies on the storage.objects table (see Permissions).
Uploading
From the browser
import { supabase } from '@/lib/supabase';
async function uploadAvatar(file: File, userId: string) {
const path = `${userId}/avatar.png`;
const { data, error } = await supabase.storage
.from('avatars')
.upload(path, file, {
cacheControl: '3600',
upsert: true,
contentType: file.type,
});
if (error) throw error;
return data.path;
}upsert: true overwrites an existing file at the same path. The browser SDK handles multipart uploads automatically for files larger than 6MB.
From the server
Inside an edge function or server action, use the service-role client to upload on behalf of a user:
import { createClient } from '@supabase/supabase-js';
const admin = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
);
await admin.storage
.from('reports')
.upload(`${orgId}/q3.pdf`, pdfBytes, {
contentType: 'application/pdf',
});Permissions
Two layers of access control:
- Bucket-level — public vs. private (the default read access policy)
- RLS policies on
storage.objects— fine-grained per-row rules
A typical "users own their folder" pattern looks like this:
create policy "Users can read own files"
on storage.objects for select
using (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = auth.uid()::text
);
create policy "Users can upload to own folder"
on storage.objects for insert
with check (
bucket_id = 'avatars'
and (storage.foldername(name))[1] = auth.uid()::text
);The agent will write these for you — just describe the rule. ("Only the user who uploaded a file can delete it" or "Anyone in the same workspace can read these.") See /docs/cloud/database for more on RLS and auth.uid().
For temporary access to a private file, generate a signed URL:
const { data } = await supabase.storage
.from('reports')
.createSignedUrl('q3.pdf', 60 * 60); // expires in 1 hourCDN delivery
Public files are served from cdn.swarmz.net/{project_id}/{path} via ~50 edge locations worldwide. The CDN caches based on the cacheControl header you set at upload time — set it generously for immutable content (max-age=31536000, immutable) and conservatively for things that may change.
Build a CDN URL like this:
const { data } = supabase.storage
.from('avatars')
.getPublicUrl(`${userId}/avatar.png`);
// data.publicUrl → https://cdn.swarmz.net/<project_id>/avatars/<userId>/avatar.pngCache invalidation happens automatically when you update or remove a file at the same path.
Image transforms
Resize, reformat, and recompress images at request time by appending query params to the public URL:
const url = `${publicUrl}?width=600&height=600&format=webp&quality=75`;Supported params:
| Param | Values |
|---|---|
width | pixels (1–4000) |
height | pixels (1–4000) |
format | webp, avif, jpeg, png |
quality | 1–100 |
resize | cover, contain, fill |
Transforms are computed on demand at the edge, then cached. The first request takes ~200ms; subsequent identical requests are served from cache in single-digit milliseconds.
File size and plan limits
Both per-file and total-storage limits are enforced. Uploads that exceed either return a 413 response.
| Plan | Total storage | Per-file limit |
|---|---|---|
| Free | 1 GB | 50 MB |
| Pro | 50 GB | 500 MB |
| Business | 500 GB | 5 GB |
| Enterprise | Custom | Custom |
Egress (CDN bandwidth) is metered separately — see your billing page for current usage.
Lifecycle and expiry
Set an expiresAt timestamp on any object to schedule automatic deletion. Useful for temp files, one-time downloads, ephemeral previews:
await supabase.storage
.from('temp-uploads')
.upload(`session/${id}.zip`, blob, {
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
});A background sweeper deletes expired objects every 15 minutes. Once deleted, the underlying CDN cache entries are also invalidated. To list or programmatically purge old files, use supabase.storage.from(bucket).list() with a prefix and iterate.