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.
What maps to what
Section titled “What maps to what”| 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 |
1. Preview and run
Section titled “1. Preview and run”Install the vaif CLI:
npm install -g @vaifstudio/cliThen run this in the Firebase project’s root, the folder with firebase.json:
vaif loginvaif migrate --from firebase --dry-runvaif 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.
2. Give the tables real columns
Section titled “2. Give the tables real columns”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.
3. Copy the data
Section titled “3. Copy the data”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.
4. Update your code
Section titled “4. Update your code”| 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 }) |
Functions
Section titled “Functions”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.