Copy Supabase Storage Buckets & Edge Functions
Published Jul 5, 2026 · 8 min read
To copy Supabase Storage buckets and Edge Functions to another project, you need three separate moves: pull function code with supabase functions download and push it with supabase functions deploy --project-ref <target>, transfer files via the Storage API or the S3-compatible endpoint (rclone works), and recreate bucket settings plus storage RLS policies in the target. A database dump alone moves none of the actual files, and Edge Function secrets can never be exported — they must be re-set by hand in the target project.
This guide walks through each path with real commands, the settings people forget (bucket limits, storage policies, the verify-JWT flag), and how SupaClone's Storage and Edge Functions add-ons automate most of it. If you're migrating an entire project, start with the full walkthrough in How to clone a Supabase project — this post zooms in on the two pieces that live outside your Postgres database.
Why doesn't pg_dump copy Supabase Storage files?
Because Supabase Storage only keeps metadata in Postgres. The storage schema has two central tables: storage.buckets (bucket name, public flag, file_size_limit, allowed_mime_types) and storage.objects (one row per file: bucket_id, name, path_tokens, owner, timestamps, a metadata JSON blob). The actual file bytes live in an S3 backend that Supabase manages for you (storage schema design).
So if you pg_dump the storage schema and restore it elsewhere, you get rows that claim files exist — pointing at objects that were never transferred. Downloads will 404 even though the dashboard lists the files. That's worse than copying nothing, because it fails silently until a user hits a broken URL.
Two more reasons a plain dump doesn't work here:
storageis a Supabase-managed schema. The target project already ships with its ownstoragetables, migrations, and triggers. Restoring your dump on top producesalready existsconflicts or, worse, a version mismatch with the target's storage service. This is the same class of problem covered in our pg_dump and pg_restore guide.- Supabase says so themselves. Even the official Restore to a new project feature explicitly does not copy storage files, bucket configurations, or Edge Functions — those are documented as manual follow-up work.
Rule of thumb: never write to storage.objects with SQL. All file operations must go through the Storage API (or the S3 protocol) so metadata and actual objects stay consistent.
How do I copy Edge Functions to another Supabase project?
Edge Functions are the easier half — the Supabase CLI has first-class commands for this (official troubleshooting guide):
supabase login
# 1. See what exists on the source
supabase functions list --project-ref <source-ref>
# 2. Download the code into supabase/functions/
supabase functions download my-function --project-ref <source-ref>
# repeat per function — there is no "download all" in one call
# 3. Deploy everything in supabase/functions/ to the target
supabase functions deploy --project-ref <target-ref>
If your functions already live in a git repo (they should), skip the download step and deploy straight from source control. The dashboard also offers a download-as-zip button per function and drag-and-drop deploy on the target, which works fine for one or two functions but gets tedious fast.
Two things this does not carry over — both covered below: the verify-JWT setting and every secret.
How do I copy Storage buckets and files to another project?
There is no supabase storage clone command. You have two realistic manual paths.
Path 1: script it with supabase-js
Loop over buckets and objects with two clients — a source client that downloads, a target client that uploads with the service role key. Supabase's own migration guide ships a script that does exactly this. The skeleton:
const { data: buckets } = await srcClient.storage.listBuckets()
for (const bucket of buckets) {
await dstClient.storage.createBucket(bucket.name, {
public: bucket.public,
fileSizeLimit: bucket.file_size_limit,
allowedMimeTypes: bucket.allowed_mime_types,
})
// recursively list(), then per object:
const { data: blob } = await srcClient.storage.from(bucket.name).download(path)
await dstClient.storage.from(bucket.name)
.upload(path, blob, { contentType, cacheControl, upsert: false })
}
The parts that bite in practice: list() is paginated and folder-scoped, so you need real recursion; contentType and cacheControl are lost unless you explicitly re-set them on upload; and you need retry/error tracking, because one failed file in ten thousand is easy to miss.
Path 2: S3-to-S3 sync with rclone
Supabase Storage speaks the S3 protocol at https://<project-ref>.storage.supabase.co/storage/v1/s3 (S3 compatibility). Generate S3 access keys for both projects under Storage → S3 Configuration, configure two rclone remotes, then:
rclone sync source:my-bucket target:my-bucket --progress --checksum
This is the fastest option for large buckets and is the approach Supabase documents for copying storage off the platform. Caveats: you must create each bucket in the target first (rclone syncs objects, not bucket settings), and S3 credentials are per-project, so keep the two remotes clearly named unless you enjoy syncing production into itself.
Which bucket settings and storage RLS policies do I need to recreate?
Moving the bytes is only half the job. Each bucket carries settings that change behavior:
public— whether objects are readable without authfile_size_limit— per-bucket upload capallowed_mime_types— upload content-type allowlist
And access control lives in RLS policies on storage.objects (and sometimes storage.buckets). These are regular Postgres policies — CREATE POLICY ... ON storage.objects — but because they sit inside a managed schema, standard schema dumps that skip storage drop them, and dumps that include storage collide with the target's own objects. You end up extracting the policies separately from pg_policies and replaying them. If a bucket was private in the source and your target has no policies, every authenticated download breaks; if you forget and mark the bucket public to "fix" it, you've just opened prod files to the world. The extraction-and-replay workflow is the same one described in copying RLS policies between Supabase projects.
What happens to Edge Function secrets and the verify-JWT flag?
Secrets do not travel — by design. Supabase secrets are write-only: you can supabase secrets list the names, but no API or CLI command returns the values (secrets docs). To get them into the target you re-set them from your own records:
supabase secrets set --project-ref <target-ref> --env-file ./prod.env
# or individually:
supabase secrets set STRIPE_SECRET_KEY=sk_live_... --project-ref <target-ref>
If the values only ever existed in the source dashboard, you'll be rotating keys at your providers. Treat this as a forcing function to keep a sealed .env per environment. Note that platform secrets like SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are injected automatically per project — never copy those; the target's own values are the correct ones.
Verify-JWT is the second silent diff. Each function either requires a valid JWT on invocation or doesn't, controlled at deploy time with --no-verify-jwt or per function in config.toml:
[functions.stripe-webhook]
verify_jwt = false # webhooks send no Supabase JWT
Deploy a webhook handler without this and Stripe gets 401 Invalid JWT on every event. Deploy an internal function with --no-verify-jwt by accident and you've removed its auth gate. Check the flag per function in the source before deploying to the target.
How does SupaClone automate Storage and Edge Function cloning?
SupaClone clones a Supabase project's structure — schemas, tables, RLS policies, functions, triggers, indexes, views, enums, extensions — into a fresh, empty target project via native pg_dump/pg_restore with a baseline-aware plan that skips managed schemas correctly. On top of that core, two optional add-ons cover exactly what this post is about:
- Storage add-on: recreates buckets with their settings, copies the files, and carries over storage RLS policies.
- Edge Functions add-on: copies function code and preserves each function's verify-JWT setting.
Every run ends with field-by-field verification and a report listing what was cloned, skipped, or failed — and, crucially, the manual steps. Secrets are never copied (they can't be — they're write-only), so each secret shows up in the report as a named manual step instead of being silently dropped. Connection is via Supabase OAuth, no connection strings.
| Manual (CLI + scripts) | SupaClone add-ons | |
|---|---|---|
| Bucket settings (public, size limit, MIME types) | Recreate by hand or script | Cloned automatically |
| Storage files | supabase-js script or rclone S3 sync | Cloned automatically |
| Storage RLS policies | Extract from pg_policies, replay | Cloned with the schema |
| Edge Function code | functions download + deploy per function | Cloned automatically |
| Verify-JWT flag | Check and re-set per function | Preserved per function |
| Secrets | Re-set by hand (write-only) | Re-set by hand — listed as manual steps in the report |
| Verification | DIY spot checks | Field-by-field report per run |
Honest scoping: if you need to move two functions and one small bucket once, the CLI plus a short script is free and fine. The add-ons pay off when you rebuild staging environments repeatedly, or when a missed policy or JWT flag in the target would be expensive. Data cloning (exact and anonymized) is coming soon but not shipped today. There's a 14-day free trial with one successful clone included if you want to test it against your own project.
FAQ
Can I copy a Supabase Storage bucket with pg_dump?
No. pg_dump only exports the metadata rows in storage.buckets and storage.objects; the file bytes live in Supabase's S3 backend and never enter Postgres. Restoring the rows without the files leaves you with a dashboard full of entries that 404 on download. Move files through the Storage API or the S3 protocol instead.
Is there a single CLI command to migrate all Storage files between projects?
No. The Supabase CLI has no storage sync command. Your options are a supabase-js script that downloads from the source and uploads to the target, or rclone against both projects' S3-compatible endpoints (/storage/v1/s3) after generating S3 access keys for each.
How do I move Edge Function secrets to another project?
You re-enter them — there is no export. Secrets are write-only: supabase secrets list shows names but never values. Run supabase secrets set --env-file ./prod.env --project-ref <target-ref> from a securely stored env file, or rotate the keys at their providers if the values were only in the source dashboard.
Why does my Edge Function return 401 after copying it?
Almost always the verify-JWT flag. The function was deployed to the source with --no-verify-jwt (or verify_jwt = false in config.toml) because it receives webhooks, and you deployed it to the target with verification on. Redeploy with the flag set to match the source.
Does SupaClone copy my Edge Function secrets?
No — and neither can anything else, because Supabase never exposes secret values after they're set. SupaClone copies function code and the verify-JWT setting, then lists every secret by name as a manual step in the clone report so nothing gets forgotten.