React + Firebase: Architecture Decisions for a Production ERP
Source: Dev.to
Building an ERP Is Not a Side Project
It handles real money, real tax obligations, and real business data. Every architecture decision has consequences that compound over months.
Below is the stack I chose for Frihet and the reasoning behind each decision.
The Stack
Frontend: React 18 + TypeScript + Vite 5
Styling: Tailwind CSS + shadcn/ui + Radix
Backend: Firebase (Auth, Firestore, Cloud Functions, Storage)
AI: Google Gemini (2.5 Flash + Pro)
Payments: Stripe Billing + Connect
Hosting: Vercel (frontend) + GCP europe‑west1 (backend)
Mobile: Capacitor (iOS + Android from same codebase)Why Firebase Over a Traditional Backend
I am a solo developer building a product that competes with companies backed by tens of millions. I cannot operate database clusters, manage migrations, handle connection pooling, and build features at the same time.
Firebase gives me:
- Auth – email, Google, GitHub, Microsoft (zero custom code)
- Firestore – realtime subscriptions; data changes propagate instantly to all connected clients
- Cloud Functions – server‑side logic; scales to zero, no idle costs
- Security Rules – enforce data isolation at the database level, not in application code
The trade‑off is vendor lock‑in and a NoSQL data model. For a solo founder shipping fast, this trade‑off makes sense.
The NoSQL Challenge
ERPs are traditionally relational. Making Firestore work required strict data‑modeling discipline:
- Workspace isolation at the database level – each business tenant has its own data partition. Even if application code has a bug, cross‑tenant data leakage is impossible because security rules enforce it.
- Denormalize for reads, reference for writes – invoice list views show the client name without an extra query, but mutations always go through the canonical client record.
- Cloud Functions for complex aggregations – anything resembling a SQL
JOINorGROUP BYruns server‑side, not client‑side.
This approach works well for ~80 % of use cases. The remaining ~20 % (complex reporting, cross‑entity queries) requires more engineering effort than it would with PostgreSQL.
State Management Without a State Library
No Redux. No Zustand. No MobX.
Firebase is the state store. The data flow is:
User action → hook → Firestore write → onSnapshot → React re‑renderThere is no local state to synchronize because Firestore is the source of truth. Optimistic updates handle the latency gap; when the write confirms, the snapshot listener fires and React re‑renders with the persisted data.
This eliminates an entire category of bugs: stale state, sync conflicts, cache invalidation. The database is always right.
Financial Calculations: One Source of Truth
Spain has VAT (21 / 10 / 4 %), the Canary Islands have IGIC (7 / 3 / 0 %), and freelancers have IRPF withholding (‑15 %). One wrong rounding operation means a tax‑filing error.
Every tax calculation, total computation, and rounding operation is centralized. Never inline math in components. Fixing a tax bug fixes it across every invoice, quote, expense, and report simultaneously.
// Every financial operation goes through the same engine
import { calculateTotal } from '@/lib/calculations';
// Whether it is an invoice, quote, or expense
const result = calculateTotal(lineItems, { taxType, withholding });AI as a First‑Class Citizen, Not a Bolt‑On
The AI copilot is not a chatbot wrapper around the UI. It has typed function definitions that operate on real user data through the same APIs the frontend uses.
Key architectural decisions
- Function calling, not RAG – the AI calls structured functions with typed parameters.
This is unambiguous; a natural‑language search is not.create_invoice({ clientId, items, taxRate }) - Lazy loading – the AI module is large; it loads only when the user first interacts with the chat. Preloading starts on hover over the chat button, so by the time they click, the module is ready.
- Context injection – the system prompt includes the user’s actual business context (active clients, recent invoices, tax obligations). The AI operates on real data, not guesses.
- Security boundaries – the AI uses the same permission model as the UI. It cannot access data the user cannot access. Function calls go through the same security rules.
i18n at Scale: 17 Languages
- Not machine‑translated labels – full localization, including fiscal terminology.
- Translation pipeline: a 240 K‑entry Translation Memory (97.8 % match rate) first, then AI only for gaps.
- Cost dropped from $3.36 to $0.30 per full translation run – a 91 % reduction.
- All 17 languages are maintained in parity. A CI check blocks any commit that has missing translation keys.
Performance Results
| Metric | Result |
|---|---|
| Lighthouse (desktop) | 100 / 100 / 100 / 100 |
| First Contentful Paint | The architecture you can ship with is better than the architecture you are still building. |
Part 3 of “Building an AI‑Native ERP”. Previously: 55‑Tool MCP Server · Why I Built It Solo.