Unbase

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 --registry

This 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 URLs

Run rewrite before load so the rows that land in your target already point at the new storage.

On this page