Migrate storage

Copy Supabase Storage buckets then objects file by file. Idempotent bucket creation, signed-URL downloads, and re-upload to your own project.

Storage migration copies your Supabase Storage buckets and every object inside them from the source project to the destination. It runs in two passes: buckets first, then objects file by file. This is a Pro-tier step.

Order: buckets, then objects

Every object row references a bucket_id. Create the buckets before you copy files, otherwise the object insert fails with a foreign-key violation. See foreign-key violation if you hit that.

The source is read-only throughout. Files are downloaded via signed URLs and uploaded to the destination — nothing is deleted or modified on the source side.

Create buckets idempotently

Bucket rows live in storage.buckets. The primary key is the bucket id, so a plain insert on a re-run throws:

duplicate key value violates unique constraint "buckets_pkey"

Insert with ON CONFLICT DO NOTHING so re-runs are safe:

insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
values ('avatars', 'avatars', true, 5242880, null)
on conflict (id) do nothing;

Preserve public, file_size_limit, and allowed_mime_types exactly — a public bucket that comes back private breaks image URLs in your app. If you see the duplicate-key error anyway, read duplicate key buckets_pkey.

Copy objects file by file

There is no bulk file transfer. For each bucket, list objects, generate a signed URL on the source, download the bytes, and upload to the destination with the same path:

const { data } = await source.storage.from('avatars').list('', { limit: 1000 })

for (const file of data) {
  const { data: signed } = await source.storage
    .from('avatars')
    .createSignedUrl(file.name, 60)

  const bytes = await fetch(signed.signedUrl).then(r => r.blob())

  await dest.storage.from('avatars').upload(file.name, bytes, {
    contentType: file.metadata.mimetype,
    upsert: true,
  })
}

upsert: true makes each upload idempotent, so an interrupted run resumes without erroring on files already copied. List folders recursively — list() returns one level at a time.

SupaMigrate runs this loop for every bucket, retrying transient upload failures per file and reporting progress from migration_steps.

Verify

Check counts match:

select bucket_id, count(*) from storage.objects group by bucket_id;

Row counts on source and destination should be equal per bucket. Next, run validation to test the destination end to end.

Edit this page on GitHubLast updated July 13, 2026