Copy Supabase Storage Buckets & Edge Functions
Published Jul 5, 2026 · Updated Jul 26, 2026 · 9 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 native Storage and Edge Functions cloning automates 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.
There's a second, sneakier variant of the same failure: your own tables usually store full storage URLs - avatar links, attachment references, JSON payloads. Copy the files and the data, and those rows still read https://<old-ref>.supabase.co/storage/.... The clone quietly keeps serving files from the old project until someone deletes it and everything 404s at once.
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 own schemas - tables, RLS policies, functions, triggers, indexes, views, enums, extensions - and, optionally, their data, into a fresh, empty target project via native pg_dump/pg_restore with a baseline-aware plan that skips the Supabase-managed schemas correctly. Two native, selectable options cover exactly what this post is about:
- Storage: recreates buckets with their settings, copies the files, and carries over storage RLS policies.
- Edge Functions: copies function code and preserves each function's verify-JWT setting.
Data clones close the loop on stored URLs, too: storage links saved inside your table rows (https://<source-ref>.supabase.co/storage/...) are rewritten to the target project ref during the clone, so copied rows point at the copied files instead of the source project.
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 (native) | |
|---|---|---|
| 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 |
| Storage URLs stored in table data | Find-and-replace SQL by hand | Rewritten to the target during data clones |
| 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. Native Storage and Edge Functions cloning pays off when you rebuild staging environments repeatedly, or when a missed policy or JWT flag in the target would be expensive. Exact data cloning is available now; anonymized data cloning is coming soon. Plans start at $9.99 one-time for 1 full clone run (only successful runs count) 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.
Do storage URLs stored in my table data still work after cloning?
With a manual migration, no - rows keep pointing at https://<old-ref>.supabase.co/... and you have to find-and-replace them yourself. SupaClone data clones rewrite URLs that reference the source project to the target project ref, so copied rows point at the copied files. Signed URLs still need re-generating (their tokens were issued by the source project), and URLs behind custom domains are not rewritten.
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.