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.
Here is what 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 with email, Google, GitHub, Microsoft — zero custom code
- Firestore with realtime subscriptions — data changes propagate instantly to all connected clients
- Cloud Functions for server-side logic — scales to zero, no idle costs
- Security Rules that enforce data isolation at the database level, not in application code
The tradeoff is vendor lock-in and a NoSQL data model. For a solo founder shipping fast, this tradeoff 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 the 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%), Canary Islands has 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. This means 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.
The key architectural decisions:
Function calling, not RAG. The AI does not search through documents. It calls structured functions with typed parameters.
create_invoice({ clientId, items, taxRate })is unambiguous. A natural language search is not.Lazy loading. The AI module is large. It only loads when the user first interacts with the chat. Preloading starts on hover over the chat button — 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 does not guess — it operates on real data.
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.
The translation pipeline uses a 240K-entry Translation Memory first (97.8% match rate), then AI only for gaps. This dropped the cost 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
- Lighthouse desktop: 100/100/100/100
- First Contentful Paint: under 1 second
- Build time: under 10 seconds (TypeScript check + Vite bundle)
- Code splitting: every view is a lazy route, AI loads on demand
The lesson
Firebase is not the "right" choice for an ERP. PostgreSQL would make reporting easier. A traditional backend would give me more control. But Firebase let me ship a production ERP with 17 languages, 41 integrations, full accounting, and AI — as a solo developer — in under 3 months.
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. Next: why the traditional ERP is dead.
Top comments (0)