Deployment

Personal Catalogue: React & Firebase

Chapter 9 · Deployment

This chapter gets the whole app running as a real standalone deployment — Chapter 10's own capstone handles the more specific question of mounting it inside the user's existing Astro-based site. Getting there is dramatically shorter than any of this project's three siblings, for one direct, cumulative reason: this course never built a server of its own to deploy in the first place.

A Genuinely Simpler Deployment — By Design, Not by Luck

Production concernThe MongoDB sibling needsThis course needs
A running backend processExpress, kept alive with PM2Nothing — Firestore is already a running, managed service
Cross-origin request handlingCORS middleware, tightened for productionNothing — the Firestore client SDK isn't a cross-origin request in the CORS sense at all
A public-facing reverse proxynginx, for a real domain and TLS terminationNothing — Firebase Hosting provides both directly
A scoped, least-privilege database accountA dedicated MongoDB user with a readWrite-only roleNothing new — already done, as Security Rules, back in Chapters 3 and 5
Serving the built frontendexpress.static(), from the same process as the APIFirebase Hosting — a dedicated static-file CDN

None of this is a shortcut this course is taking — it's the direct, cumulative consequence of Chapter 3's own real decision that this app never needed a backend server at all.

Isolating Dev From Production: a Different Reason for Env Variables Than the MongoDB Sibling's

The MongoDB sibling's own Chapter 9 moves six hardcoded localhost:4000 URLs into one environment-driven constant, because a real production domain has to replace a literal development address. This course has no API URLs to centralize — there's no backend of its own to point at. What genuinely is worth moving into environment variables here is something different: the Firebase project config itself, so a production build can point at a completely separate Firebase project than local development uses, keeping real production data untouched by anything built or tested locally.

// src/firebase.js import { initializeApp } from "firebase/app"; import { getFirestore } from "firebase/firestore"; const firebaseConfig = { apiKey: import.meta.env.VITE_FIREBASE_API_KEY, authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID, }; const app = initializeApp(firebaseConfig); export const db = getFirestore(app);
# .env.development — a real, separate development Firebase project VITE_FIREBASE_API_KEY=AIzaSy...dev VITE_FIREBASE_AUTH_DOMAIN=catalogue-dev.firebaseapp.com VITE_FIREBASE_PROJECT_ID=catalogue-dev # .env.production — the real production project VITE_FIREBASE_API_KEY=AIzaSy...prod VITE_FIREBASE_AUTH_DOMAIN=catalogue-prod.firebaseapp.com VITE_FIREBASE_PROJECT_ID=catalogue-prod
Isolation, Not Secrecy — a Direct Callback to Chapter 1
Chapter 1's own warn-box already established that this config object was never a secret in the first place — it identifies a project, it doesn't grant access. Moving it into environment variables here isn't about hiding anything; it's purely so npm run dev and a real production build can point at two genuinely separate Firebase projects, so testing locally never risks touching or corrupting real catalogue data.

Building & Deploying to Firebase Hosting

# from the project root npm run build

Vite's default build writes a real, deployable set of static files into dist/. Firebase Hosting is configured once, per project, to serve exactly that folder:

// firebase.json { "hosting": { "public": "dist", "rewrites": [{ "source": "**", "destination": "/index.html" }] } }
The Rewrite Rule Above Is a No-Op Here — the Same Reason as Chapter 7
Firebase Hosting's own rewrites option exists for the same reason most SPA-hosting setups need one: a real client-side route like /books/123 would otherwise 404 on a hard refresh, since no static file at that exact path exists. This app never had a client-side route at all — Chapter 7's own list/detail toggle is pure component state, never a URL change — so this rule, while harmless to include, never actually has anything to rewrite in practice.
# deploy the built frontend firebase deploy --only hosting # or deploy hosting AND any pending Security Rules changes together firebase deploy

What This Course Never Needed to Build

No CORS configuration exists anywhere in this app, in any chapter — the Firestore client SDK talks to Firebase's own servers using its own protocol, not a same-origin-restricted browser fetch, so there was never a cross-origin policy of this app's own to write in the first place. No PM2, because there is no long-running process of this course's own to keep alive; Firestore and Firebase Hosting are already running, managed services maintained by Firebase itself. No nginx reverse proxy, because Firebase Hosting already terminates TLS and serves the app directly.

The "Least-Privilege Access" Work Already Happened — Earlier
The MongoDB sibling creates a dedicated, restricted database user in this exact chapter. This course has no equivalent user account to create at all — Firestore has no login-based database-user model the way MySQL or MongoDB does. The real equivalent work already happened, earlier, as Security Rules: Chapter 3's own hasValidBaseFields()/hasTypeRequiredFields()/hasValidResolution() and Chapter 5's own tagsOnlyOnBooks() are this app's real access-control layer, and nothing new needs to be written here to reach production — only confirming those rules are actually deployed to the real production project, not just the development one used throughout this course so far.

Automatic HTTPS, for Free

Firebase Hosting provisions and renews a free SSL certificate automatically for both its own *.web.app subdomain and any real custom domain connected to the project — no certificate request, no renewal cron job, and no dedicated reverse proxy layer required to get it, unlike the MongoDB sibling's own nginx-plus-certificate setup.

Scheduled Backups, the Managed Way

The MongoDB sibling schedules a real mongodump cron job to keep a dated snapshot on disk, independent of whatever a hosting provider's own backup story does or doesn't cover. Firestore offers a real, equivalent managed feature — scheduled backups, configured directly through Google Cloud rather than a self-written script:

# a real, one-time setup command — Google Cloud manages the schedule from here gcloud firestore backups schedules create \ --database='(default)' \ --recurrence=daily \ --retention=7d

The same underlying need — a dated snapshot existing independently of the live database, in case something ever needs to be restored — solved here by configuring a managed service once, rather than maintaining a script and a cron entry indefinitely.

Trying It

npm run build firebase deploy --only hosting

Visiting the real https://catalogue-prod.web.app URL Firebase prints after deployment should load the working catalogue app directly, with every action — search, add, tag edit — succeeding exactly as it did throughout development, now reading from and writing to the real production Firestore project instead of the development one.

Hands-On Exercises

Exercise 1

Move the Firebase config in firebase.js into VITE_-prefixed environment variables, create separate .env.development and .env.production files pointing at two different real Firebase projects, and confirm npm run dev connects to the development project while a production build connects to the other.

📄 View solution
Exercise 2

Build the app, configure firebase.json for Hosting, and deploy it with firebase deploy --only hosting. Confirm the real, live URL Firebase provides loads the working app and that a real add-item action succeeds against the deployed site.

📄 View solution
Exercise 3

Explain, for each of CORS, PM2, and a reverse proxy, precisely why this course never needed to build it — tying each answer back to a specific earlier architectural decision rather than a general "Firebase handles it" claim.

📄 View solution

Chapter 9 Quick Reference

  • No backend to deploy — the direct, cumulative payoff of Chapter 3's own no-server decision
  • Env-driven Firebase config — for real dev/prod project isolation, not for secrecy (Chapter 1 already settled that question)
  • Firebase Hosting — npm run build, then firebase deploy --only hosting
  • The SPA rewrite rule is a no-op here too — the same real reason as Chapter 7's own missing fallback route: no client-side routing exists in this app at all
  • Never needed — CORS, PM2, an nginx reverse proxy, a dedicated database user (Security Rules already cover that job, done back in Chapters 3 and 5)
  • Free, automatic HTTPS — provisioned and renewed by Firebase Hosting itself
  • Managed backups — Firestore's own scheduled backup feature, configured once rather than maintained as a cron script
  • Next chapter: Capstone — integrating the catalogue into the existing Astro site, and comparing self-managed MongoDB against managed Firestore across the whole project