React + Firebase: Architecture Decisions for a Production ERP

Published: (April 3, 2026 at 06:00 AM EDT)
5 min read
Source: Dev.to

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 JOIN or GROUP BY runs 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‑render

There 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

  1. Function calling, not RAG – the AI calls structured functions with typed parameters.
    create_invoice({ clientId, items, taxRate })
    This is unambiguous; a natural‑language search is not.
  2. 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.
  3. 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.
  4. 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

MetricResult
Lighthouse (desktop)100 / 100 / 100 / 100
First Contentful PaintThe 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.

Next: Why the traditional ERP is dead

Frihet — Free, AI‑native ERP.
Try it · Docs · npm

0 views
Back to Blog

Related posts

Read more »