Firebase vs PocketBase: Self-Hosting Behind a Serverless Front End
PocketBase is one Go binary with embedded SQLite; Firebase is a managed bundle. The axis nobody compares: what each costs when your front end holds no state.
PocketBase is one Go binary: SQLite, realtime subscriptions, auth, file storage and an admin dashboard in a single executable you run yourself. Firebase is the opposite trade — none of it runs on your machine, and none of it is yours to fix. Every comparison covers that. What almost none of them cover is the part that decides it for a lot of real projects: how each one behaves when the thing calling it is serverless.
The short answer
Choose PocketBase if you want one process, one file, full control of your data, and an ops bill measured in your own time. Choose Firebase if you want managed scaling, mobile SDKs with offline sync, and no server to patch — paid for in usage billing and vendor lock-in.
Then check the question underneath both: your front end probably runs as many short-lived instances with no shared memory and no persistent disk. That constraint invalidates a chunk of the advice written for each of these backends.
The comparison
| Firebase | PocketBase | |
|---|---|---|
| Runs where | Google's infrastructure, managed | One binary on your server |
| Database | Firestore / RTDB documents (Postgres via Data Connect) | Embedded SQLite, one file |
| Auth | Firebase Auth, including phone and federated | Built in: email/password, OAuth2, OTP |
| File storage | Cloud Storage, with Security Rules | Built in: local disk or S3-compatible |
| Realtime | Mature snapshot listeners, offline persistence | Server-sent-event subscriptions |
| Authorization | Security Rules, a declarative DSL | API rules per collection, filter expressions |
| Horizontal scaling | Transparent and effectively unbounded | Vertical only — one writer, one process |
| Licence / exit | Proprietary service | MIT; your data is a .db file |
| Backups | Managed export, configurable | Yours to script and verify |
| Cost shape | Usage-based, scales with traffic | A VPS, plus your time |
| Mobile offline sync | The strongest in the category | Not a goal of the project |
Where this audit comes from
This storefront runs Postgres on Supabase behind Next.js on Vercel, so it is a third answer — but it is one that has been through both halves of the problem these two products trade against, and the numbers below are measured here rather than quoted.
Relevant shape: 111 products, 110 of them live, and a front end where src/proxy.ts — 42 lines — executes on every single request to refresh the session cookie. That file is also the project's clearest lesson about managed versus self-run infrastructure, and it is written into the repo's own CLAUDE.md as a deploy warning: if its two environment variables are absent, every route on the site errors, not just the account pages. A backend that your request path depends on is a backend whose availability is your availability. That is true of a Firebase outage and true of your own PocketBase box, with the difference that only one of them is yours to restart.
The assumption PocketBase makes, and the one serverless breaks
PocketBase is a single process. That is the source of its best properties — no network hop to the database, no connection pool, transactions for free — and it quietly licenses a habit that will not survive contact with a serverless front end: keeping state in memory.
This codebase had to learn the same rule from the other side. Downloads are rate-limited per buyer per hour, and the limit cannot live in a variable, because the function enforcing it is a different instance every few requests. So it counts rows:
-- durable download rate limit (R-13): counts from the table, not memory
create or replace function public.downloads_in_last_hour(uid uuid)
returns int language sql stable security invoker
set search_path = public, pg_temp as $$
select count(*)::int from public.download_events
where user_id = uid and created_at > now() - interval '1 hour';
$$;
Three lines of SQL, and the comment above them is the whole design note. The table is the memory, because the application has none it can trust.
Here is the part that matters for this comparison. If your backend is PocketBase, you could hold that counter in the Go process, since there is exactly one — and that would be correct, and faster. But if your front end is a Next.js app on serverless, the counter cannot live there, so it has to live in the backend anyway, which means you are making the same call against PocketBase that we make against Postgres. The single-process advantage is real for the backend's own internals and evaporates the moment the state is produced by callers it does not control.
There is a second edge in the same family. Counting and then writing is two steps, and two steps race: a burst of parallel requests can all read the same count, all pass the check, and all insert. The fix here was to make the count and the insert one call, serialized per user with a transaction-scoped advisory lock — the full post-mortem is in Firebase vs Convex, which is where the declarative-rules-versus-functions version of this argument lives. The relevance here: Firestore's answer is a transaction, PocketBase's answer is that SQLite has a single writer anyway, and a managed service's answer is always one you have to look up. On a self-hosted single-writer database, a whole class of concurrency bug is structurally impossible. That is a genuine PocketBase advantage and it is almost never listed, because it only shows up when you read a failure report instead of a feature grid.
Private files: a bucket, or a folder you own
Everything this storefront sells is a zip, and none of it may ever be publicly fetchable. The write path is a script with a service-role key:
const admin = createClient(url!, key!, {
auth: { persistSession: false, autoRefreshToken: false },
});
const { error } = await admin.storage
.from("releases")
.upload(objectPath, readFileSync(localZip), {
upsert: true,
contentType: "application/zip",
});
That is scripts/release/upload.ts, uploading into a bucket created with public = false. The read path, in src/app/api/download/route.ts, authorizes first and then mints a short-lived signed URL — the object itself is never reachable by guessing a path.
Both products cover this requirement, differently:
- Firebase gives you Cloud Storage with Security Rules and Admin-SDK signed URLs. The failure mode is a rule left permissive, which is one of the most common data exposures in the whole category.
- PocketBase stores files on the local disk by default (or an S3-compatible bucket if you configure one) and protects them with per-collection API rules plus short-lived file tokens. The failure mode is different and more physical: the files are on a disk you are responsible for backing up, and "the server died" and "the files are gone" can be the same event.
If you self-host, the restore drill is the deliverable, not the backup script. A backup nobody has restored is a hypothesis.
What you actually own
| Job | Firebase | PocketBase |
|---|---|---|
| Patching the runtime | You | |
| Keeping the process alive | You (systemd, Docker, a PaaS) | |
| Backups + verified restores | Managed export you still have to schedule | Entirely you; the upside is it is one file |
| Scaling a traffic spike | Automatic, and it bills | Resize the box, or shed load |
| Schema changes | Nothing to change, which is its own problem | Migrations, in the admin UI or as Go/JS files |
| Security posture review | Rules audit | Rules audit and host hardening |
| Debugging at 2am | A huge public answer corpus | Your logs, on your machine, which is often faster |
The honest summary of that table: PocketBase moves work from a bill to a calendar. For a side project, a prototype, an internal tool or anything with a known traffic ceiling, that is a good trade and the whole point of the project. For a team without an on-call rotation shipping something customers depend on, the managed column is cheaper than it looks, because the cost it removes is the one you cannot schedule.
The rule we would use
Pick PocketBase when: the data is yours and must stay that way, the traffic shape is known, one server is genuinely enough, and someone on the team is comfortable running a Linux box. Its single-binary deployment is not a toy — it is a real backend with an admin UI, and the SQLite file is the most portable production database format in existence.
Pick Firebase when: you need mobile offline sync (this is the decisive case — nothing in the self-hosted column competes), traffic is unpredictable, or nobody wants to be paged. Accept the rules DSL and the lock-in as the price.
And pick neither when your hardest requirement is relational: a purchase that must create an order and its entitlements atomically, with foreign keys deciding what happens when a user is deleted, wants a relational store. That is the audit in Firebase vs PlanetScale, and it is why this repository runs Postgres.
Troubleshooting and migration notes
| Situation | What goes wrong | What to do |
|---|---|---|
| Rate limit works in dev, fails in production | The counter lived in process memory; your front end has N instances | Count from a table, or put the limit inside the backend process |
| Firestore → PocketBase | Documents have no schema; collections do | Define collections first, then write a transform; expect to discover fields you did not know existed |
| PocketBase → Firestore | SQLite joins have nowhere to go | Denormalise at write time, and treat each copy as a consistency job |
| Self-hosted files vanish with the server | Local disk was the only copy | Configure S3-compatible storage, or back the data directory up and test the restore |
| Uploaded files are publicly guessable | A bucket or collection rule left open | Private bucket plus signed, expiring URLs; authorize before you sign |
| PocketBase under real concurrency | SQLite has one writer; long write transactions queue everything | Keep write transactions short; read replicas do not help writes |
| Firebase bill jumps after a release | A client-first screen that reads documents per render | Count reads per page, then cache or denormalise the hot one |
| Nobody can restore the backup | The script ran, the restore never did | Rehearse a restore on a fresh box, on a schedule |
Frequently asked questions
Is PocketBase production-ready? Yes, within its design envelope: one process, vertical scaling, SQLite's single writer. Projects with known traffic run on it happily. It is not trying to be a horizontally scaling platform, and reading its scaling limits as a defect misses what it is for.
Can I self-host Firebase? No. The client SDKs are open source; the service is not. The emulator suite is for local development, not production. If "we must be able to run this ourselves" is a hard requirement, the comparison is already decided.
Does PocketBase give me realtime like Firestore? It gives you realtime subscriptions over server-sent events, which covers a dashboard updating itself. It does not give you Firestore's offline-first mobile persistence — a decade of conflict resolution and local caching that is the single hardest thing in this category to replicate. If your app is mobile-first and works on a train, that is the whole decision.
We are behind Vercel or similar — does self-hosting the backend even make sense? It can, and the pairing is more common than the docs suggest: a static or server-rendered front end on a CDN, one small box running the backend. What to watch is latency between them (put them in the same region) and the fact that your serverless functions hold no state, so every limit, lock and counter belongs in the backend. That constraint is identical whichever of these two you pick, which is exactly why it is worth checking before the feature grid.
Templates in this post
ASoc Till (a point-of-sale landing page built around a live register preview), ASoc Timbre (a text-to-speech voice-generator marketing page) and ASoc Uptime (a web-hosting brand page with a live domain-search widget) ship as front ends with no backend assumptions compiled in — point them at either column above.
Browse the full sets: Next.js landing page templates, Tailwind landing page templates.
