Storage Migration
Mirror Supabase Storage to any S3-compatible bucket and rewrite URLs.
Extract & mirror
extract --mirror lists every object across all buckets (recursively) and copies
the bytes into the IR, byte-for-byte. It is resumable — existing files are skipped.
It also reconciles the bucket against your data:
- Dangling references — paths your rows point at that have no object.
- Orphan files — objects no row references.
Both are reported so you see exactly what's broken or unused before you move
anything. Without --mirror, extract still lists and reconciles, just without
downloading bytes.
Upload
storage uploads the mirrored objects to your target, skipping anything already
there (precise HEAD checks, so transient errors aren't mistaken for "missing").
Targets: Cloudflare R2, AWS S3, Google Cloud Storage, MinIO — anything S3-compatible. The source bucket name is kept as a key prefix so multiple buckets coexist in one target bucket.
Convert (optional)
Convert and optimize assets as they upload. Images use sharp
(libvips); videos transcode via a bundled ffmpeg (ffmpeg-static, no system
install needed). Converters compose — enable both at once:
npx unbase storage \
--convert-images webp --image-quality 80 --max-width 2000 \
--convert-videos webm- Images → WebP/AVIF/JPEG/PNG/keep, with optional downscale.
- Videos → MP4 (H.264) or WebM (VP9), with optional max height.
When a conversion changes a file's extension (a.png → a.webp, clip.mov →
clip.webm), Unbase records the mapping in the snapshot so the rewrite step
repoints your data's URLs to the converted object automatically. Anything a
converter doesn't handle passes through untouched. Conversion is pluggable: image
and video ship today, and new converters implement the same Converter contract
and drop into the same composeConverters([...]) pipeline.
Registry (optional)
Pass --registry to also create and populate a normalized storage_object table in
your target — a single source of truth for every file (provider, bucket, object_key,
public_url, mime_type, size, kind, status), with a unique (provider, bucket, object_key) index. Rows use the post-conversion keys/URLs, so the registry,
the uploaded bytes, and the rewritten inline URLs all agree.
npx unbase storage --convert-images webp --registryThis gives the new app a proper storage model to build on (modeled on a
production schema), instead of only loose files in a bucket. Unbase does not
auto-rewrite your domain columns into storage_object foreign keys — which column
is an image reference is app-specific — so that linking stays your choice.
Rewrite
rewrite repoints the storage URLs embedded in your rows from Supabase to the new
target's public URL, in place in the IR. It matches public, authenticated, and
signed object URLs and drops signing query strings (the target serves the object
directly).
npx unbase extract --mirror
npx unbase storage
npx unbase rewrite # run before load, so the loaded data has new URLsRun rewrite before load so the rows that land in your target already
point at the new storage.