Migrate from Firebase Storage
Copy files from Firebase Storage (Cloud Storage for Firebase) into SupaHost Storage buckets, rewrite security rules as policies and update stored download URLs.
To move files from Firebase Storage to SupaHost, download them from the underlying Cloud Storage bucket, then upload them to a SupaHost Storage bucket with a short supabase-js script. Firebase security rules become policies on storage.objects, and any download URLs saved in your database need rewriting.
Plan
- Choose buckets. Firebase apps usually have one bucket with top-level folders (
avatars/,uploads/). On SupaHost you can keep one bucket or split folders into buckets, which makes public versus private access simpler. - Check sizes. Each file can be up to 50 MB on SupaHost, and all projects on the instance share the plan's file storage (100 GB Builder, 250 GB Pro, 500 GB Business). Find large files first:
gsutil du -h gs://recipe-box-prod.appspot.com/** | sort -h | tail. - Map user folders. If paths contain Firebase UIDs (
users/{uid}/...), migrate Auth first (Migrate from Firebase Auth) so you can swap them for Supabase user IDs.
Export from Firebase
gcloud storage cp -r gs://recipe-box-prod.appspot.com ./firebase-files
(gsutil -m cp -r works too.) This copies every object with its folder structure.
Create buckets
Dashboard → your project → Studio → Storage, or in SQL:
insert into storage.buckets (id, name, public)
values ('avatars', 'avatars', true), ('uploads', 'uploads', false);
Upload
import { createClient } from '@supabase/supabase-js'
import { readFile, readdir } from 'node:fs/promises'
import { join, relative, extname } from 'node:path'
import mime from 'mime'
const admin = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!)
const root = './firebase-files/recipe-box-prod.appspot.com/uploads'
async function* walk(dir: string): AsyncGenerator<string> {
for (const e of await readdir(dir, { withFileTypes: true })) {
const p = join(dir, e.name)
if (e.isDirectory()) yield* walk(p)
else yield p
}
}
for await (const file of walk(root)) {
let path = relative(root, file).replaceAll('\\', '/')
path = path.replace(/^users\/([^/]+)/, (_, fbUid) => uidMap.get(fbUid) ?? fbUid)
const { error } = await admin.storage.from('uploads').upload(path, await readFile(file), {
contentType: mime.getType(extname(file)) ?? 'application/octet-stream',
upsert: true,
})
if (error) console.error(path, error.message)
}
upsert: true makes the script safe to rerun. Uploading with the service_role key bypasses policies, so the objects have no owner; if your policies check owner_id, write them against the path instead (as below).
Rewrite security rules
A rule like allow read, write: if request.auth.uid == userId on users/{userId}/** becomes:
create policy "users manage their own folder" on storage.objects
for all to authenticated
using (bucket_id = 'uploads' and (storage.foldername(name))[1] = (select auth.uid())::text)
with check (bucket_id = 'uploads' and (storage.foldername(name))[1] = (select auth.uid())::text);
Update stored URLs
Firebase download URLs (https://firebasestorage.googleapis.com/...?alt=media&token=...) stop working once you shut Firebase down. Store the object path instead and build URLs in the app:
const { data } = supabase.storage.from('avatars').getPublicUrl(`${userId}/avatar.jpg`)
const { data: signed } = await supabase.storage.from('uploads').createSignedUrl(path, 3600)
There are no image transformations on SupaHost, so resize images before upload if you relied on a resize extension.
Verify
select bucket_id, count(*), pg_size_pretty(sum((metadata->>'size')::bigint))
from storage.objects group by bucket_id;
Compare with the export, then open a few files as a signed-in user.