Firebase is a fantastic place to start a project and an awkward place to grow one. The vendor lock-in is real, the pay-as-you-go bill gets hard to predict once you have traffic, and you can't take Firestore with you when you leave.
I moved a small app off Firebase and onto PocketBase Cloud recently, and it was smoother than I expected. This is the practical version of how to do it — the concept mapping, the code changes, the data migration, and an honest note on when you should just stay on Firebase.
Disclosure: I work on PocketBase Cloud. I've tried to keep this a real migration guide rather than a sales pitch — including a section on where Firebase is still the better call.
Why migrate at all?
Not everyone should. But common reasons:
- Predictable cost. Firebase bills per read/write/invocation. PocketBase Cloud bills per server — you know your number in advance, and it doesn't spike with a traffic burst.
- No lock-in. PocketBase is open source and self-hostable. Your data lives in a SQLite file you can download and run anywhere. Cloud hosting is a convenience, not a cage.
- One system. Database, auth, file storage, and server-side logic in a single tool instead of four Firebase products stitched together.
Concept mapping
Most Firebase concepts have a direct PocketBase equivalent, which is what makes the move manageable:
| Firebase | PocketBase |
|---|---|
| Firestore collection | Collection (with a real schema) |
| Firestore document | Record |
| Firebase Auth | Built-in auth (email/pw, OAuth2, OTP) |
| Security Rules | API Rules (filter expressions per collection) |
| Cloud Storage | File fields on records |
| Cloud Functions | JS hooks (routes, events, cron) |
onSnapshot() realtime |
subscribe() over SSE |
The biggest mental shift: Firestore is schemaless, PocketBase collections have a schema. That feels like more work up front and saves you from a lot of "why is this field sometimes a string and sometimes null" later.
Step 1 — Deploy the backend
Sign up at PocketBase Cloud, deploy an instance (~30 seconds), and open the admin dashboard at /_/. Recreate your collections there, this time with proper field types and validation. Where Firebase made you enforce shape in Security Rules or client code, here the schema does it.
Step 2 — Swap the client SDK
Here's the before/after for the two operations you do most.
Reading a collection
// Firebase
import { collection, getDocs } from "firebase/firestore";
const snap = await getDocs(collection(db, "posts"));
const posts = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
// PocketBase
import PocketBase from "pocketbase";
const pb = new PocketBase("https://your-app.pocketbasecloud.com");
const posts = await pb.collection("posts").getFullList({ sort: "-created" });
Realtime subscription
// Firebase
import { onSnapshot, collection } from "firebase/firestore";
onSnapshot(collection(db, "messages"), (snap) => {
snap.docChanges().forEach((c) => {
if (c.type === "added") addMessage(c.doc.data());
});
});
// PocketBase (SSE under the hood)
pb.collection("messages").subscribe("*", (e) => {
if (e.action === "create") addMessage(e.record);
});
The shapes are close enough that most of the migration is find-and-replace plus adjusting field names.
Step 3 — Move auth
Firebase Auth users don't export with password hashes in a reusable form, so the usual pattern is:
- Recreate your OAuth providers (Google, GitHub, etc.) in the PocketBase dashboard — these "just work" since the user re-consents on next login.
- For email/password users, trigger a password reset flow on first login after migration, so they set a new password against the new backend.
Permissions move from Security Rules to API Rules — short filter expressions on each collection:
// "only the owner can update their own record"
@request.auth.id != "" && owner = @request.auth.id
If you've written Firestore Security Rules, this will feel familiar and considerably shorter.
Step 4 — Migrate the data
Export each Firestore collection to JSON (the Firebase CLI or a small Admin SDK script does this), then write records into PocketBase via the SDK:
import PocketBase from "pocketbase";
const pb = new PocketBase("https://your-app.pocketbasecloud.com");
await pb.admins.authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD);
const posts = JSON.parse(fs.readFileSync("posts.json", "utf-8"));
for (const p of posts) {
await pb.collection("posts").create({
title: p.title,
body: p.body,
// map Firestore fields → PocketBase fields here
});
}
For anything beyond a few thousand records, batch it and add a small delay so you don't hammer your instance. Files move the same way: download from Cloud Storage, upload as file fields.
Step 5 — Cloud Functions → hooks
Firebase Cloud Functions become PocketBase JS hooks, running inside your instance next to the data. A Firestore onCreate trigger becomes an onRecordAfterCreateSuccess hook; a scheduled function becomes a cron hook. The nice difference: hooks aren't billed per invocation, so a hot code path doesn't translate into a scary bill.
If you need heavier server-side code than hooks allow — long-running jobs, websockets, a full Node/Deno/Bun/Next.js service — the Pro plan runs those on your server too, so the whole app stays on one platform.
When you should stay on Firebase
Genuinely — don't migrate if:
- You need massive write concurrency. PocketBase is SQLite; writes serialize. Firestore scales horizontal writes in ways SQLite doesn't. A high-write, high-concurrency workload is a real reason to stay.
- You lean on the wider Firebase/GCP ecosystem — FCM push, ML Kit, BigQuery pipelines, Crashlytics. PocketBase is a focused backend, not a cloud platform.
- Offline-first mobile with automatic conflict resolution is core to your app. Firestore's offline persistence is more mature here.
For read-heavy CRUD apps — dashboards, content sites, most mobile backends — none of that bites, and you get a simpler, cheaper, portable stack.
Try it
The migration is easiest to feel by doing a throwaway one. The Free plan (1 instance + 5 frontends, stays running) is enough to recreate a collection or two and port a screen:
If you're mid-migration and hit something that doesn't map cleanly, drop it in the comments — I've been through a few of these and I'm happy to help work it out. 🚀
Top comments (0)