Swarmz

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:

  1. Bucket-level — public vs. private (the default read access policy)
  2. 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 hour

CDN 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.png

Cache 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:

ParamValues
widthpixels (1–4000)
heightpixels (1–4000)
formatwebp, avif, jpeg, png
quality1–100
resizecover, 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.

PlanTotal storagePer-file limit
Free1 GB50 MB
Pro50 GB500 MB
Business500 GB5 GB
EnterpriseCustomCustom

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.

On this page