Skip to content

Migrate from Firebase

Firebase stores documents; VAIF Studio stores rows in tables. vaif migrate --from firebase gives you a starting point: a table for each Firestore collection, storage buckets, and a function stub for each exported function. Designing typed columns, copying data, moving users and porting code are up to you, and this page walks through them.

Firebase VAIF Studio Moved by vaif migrate
Firestore collections (found in firestore.rules) Tables A table for each collection, with id, created_at, updated_at and a data JSON column
Firebase Storage paths in storage.rules Storage buckets Private buckets. Not the files
Functions in functions/index.ts Functions A stub for each exported function. You port the code
Security rules Row rules and storage policies No
Firebase Auth users Your project’s users No. See Users
onSnapshot listeners Row-change subscriptions Code change

Install the vaif CLI:

Terminal window
npm install -g @vaifstudio/cli

Then run this in the Firebase project’s root, the folder with firebase.json:

Terminal window
vaif login
vaif migrate --from firebase --dry-run
vaif migrate --from firebase --project-id <your-vaif-project-id>

--dry-run writes the plan to vaif-migration-plan.md and creates nothing. Collections are found from the match /collection/{id} blocks in firestore.rules, so a collection with no rule there is not found.

Each generated table keeps the whole document in a data JSON column. Replace it with typed columns on the console’s Schema page: a document { title, authorId, tags } becomes the columns title text, author_id uuid and tags text[]. A subcollection usually becomes its own table, with a foreign key to its parent.

Export each collection to JSON with the Firebase Admin SDK, reshape each document into a row, and insert the rows from a server with createMany() and a secret key:

import { readFileSync } from "node:fs";
import { createVaifClient } from "@vaifstudio/client";
const vaif = createVaifClient({
baseUrl: "https://api.vaif.studio",
projectId: process.env.VAIF_PROJECT_ID,
apiKey: process.env.VAIF_SECRET_KEY, // vk_srv_: server only
});
type ExportedDoc = { id: string; title: string; authorId: string; createdAt: string };
const docs: ExportedDoc[] = JSON.parse(readFileSync("posts.json", "utf8"));
const rows = docs.map((d) => ({ firebase_id: d.id, title: d.title, author_id: d.authorId, created_at: d.createdAt }));
for (let i = 0; i < rows.length; i += 500) {
await vaif.from("posts").createMany(rows.slice(i, i + 500));
}

Firebase password hashes cannot be imported. Create each user with POST /projects/{projectId}/users and a body such as { "email": "...", "metadata": { "firebaseUid": "..." } }, using a console access token of a project developer or higher. Then have each user set a new password with vaif.auth.requestPasswordReset({ email }), or sign in with a magic link.

Users who signed in to Firebase with a social account can do the same here once you set that provider up; see Social sign-in. A provider account whose verified email matches a user you imported signs in to that user once the user’s own email is verified.

Firebase @vaifstudio/client
getDoc(doc(db, "posts", id)) vaif.from("posts").get(id)
getDocs(query(collection(db, "posts"), where("published", "==", true))) vaif.from("posts").eq("published", true).list()
addDoc(collection(db, "posts"), data) vaif.from("posts").create(data)
updateDoc(ref, data) vaif.from("posts").update(id, data)
deleteDoc(ref) vaif.from("posts").delete(id)
onSnapshot(collection(db, "posts"), fn) vaif.from("posts").on("*", fn).subscribe()
createUserWithEmailAndPassword(auth, email, password) vaif.auth.signUp(email, password)
signInWithEmailAndPassword(auth, email, password) vaif.auth.login(email, password)
uploadBytes(ref(storage, path), file) upload(vaif, bucket, path, file) from @vaifstudio/client/storage
getDownloadURL(ref(storage, path)) vaif.storage.createSignedUrl(path, 3600, { bucket })

Port HTTP functions to VAIF functions. A Firestore onWrite trigger becomes a function with a row trigger on the table (db.insert, db.update or db.delete); see Run when rows change. There is no trigger for a new sign-up: run that code after your app’s sign-up call instead.