All work

Case study

Nexora

Modern ERP & CRM platform for managing customers, quotes, invoices, and payments — built with the MERN stack and Redux.

Role
Solo full-stack engineer
When
Personal project
For
Open source
  • MongoDB
  • Express
  • React
  • Node.js
  • Redux
Nexora screenshot
Nexora — MongoDB, Express, React

01The brief

The problem

Small businesses juggle customers in one tool, quotes in a spreadsheet and invoices in a word processor. Nexora puts the whole loop in one place: a customer gets a quote, the quote becomes an invoice, payments are recorded against it, and the dashboard shows where the money is.

The aim was a complete, deployable ERP/CRM rather than a demo — real PDFs, real email, settings a business would actually change, and a backend that does not need rewriting every time a new document type is added.

02In use

The product

03Architecture

How it fits together

  1. Client
    • React 18 on Vite, React Router 6
    • Redux Toolkit for app state, Ant Design 5 for forms and tables
    • Axios request layer shared by every module
  2. API
    • Express with JWT auth and bcrypt-hashed passwords
    • Generic entity routes — create, read, update, delete, list, search, filter, summary
    • Core and app controllers kept apart, so platform concerns never leak into business modules
  3. Documents
    • Pug templates rendered to PDF for invoices, quotes and payment receipts
    • Resend for emailing documents and password-reset links
  4. Data
    • MongoDB through Mongoose, with core models (admin, settings) split from app models
  5. Deploy
    • Single service: Express serves the built frontend
    • Dockerfile plus Render blueprint, seeded with one setup command

04Challenges

The hard parts

Not writing the same CRUD six times

Problem

Customers, quotes, invoices, payments, payment modes and taxes all need the same list, search, filter and paginate behaviour. Hand-writing each set of endpoints would mean six copies of the same bugs.

Approach

A generic controller factory takes a model and returns the standard handlers; entity routes are generated from it. Modules only override what is genuinely different, such as invoice totals or quote conversion.

Money that adds up

Problem

Line items, tax rates, discounts and partial payments all interact, and a total computed in the browser can be tampered with or simply drift.

Approach

The client shows a live estimate, but subtotal, tax and total are recalculated on the server on every save. Recording a payment updates the invoice's paid amount and status in the same request, so the two cannot disagree.

Quote to invoice without retyping

Problem

Converting a quote by hand invites copying errors and loses the link between the two documents.

Approach

A convert endpoint builds the invoice from the quote's customer and line items, assigns the next invoice number and marks the quote converted — one action, with a record of where the invoice came from.

Branded documents on any host

Problem

PDFs have to carry the business's name, currency format and numbering prefixes, and still render on a plain container host.

Approach

Settings feed the Pug templates at render time, so a change in Settings shows up in the next PDF without a deploy. Files are written to a downloads directory and served through one route.

05Data model

Schema decisions

Documents are self-contained: an invoice carries everything needed to reprint it years later, even if the customer or the tax rate changes.

Client

  • name
  • email
  • phone
  • address
  • removed
  • enabled

Soft-removed rather than deleted, so old invoices still resolve.

Quote

  • number
  • year
  • client → Client
  • items[]
  • taxRate
  • total
  • status
  • converted

Line items embedded; converted stops a quote being invoiced twice.

Invoice

  • number
  • year
  • client → Client
  • items[]
  • subTotal
  • taxTotal
  • total
  • credit
  • paymentStatus

Totals stored, not derived on read — the list view never recalculates.

Payment

  • invoice → Invoice
  • client → Client
  • amount
  • paymentMode → PaymentMode
  • date

Each payment moves the invoice's credit and paymentStatus.

Setting

  • settingKey
  • settingValue
  • settingCategory

Key/value, so a new option is a seed row, not a migration.

Embed line items
Items are only ever read with their document, so embedding them makes every invoice one read and keeps a document consistent with what was sent.
Reference the client
Clients are shared across many documents and get edited, so they are referenced and populated for display rather than copied.
Soft delete everywhere
A removed flag instead of a delete keeps financial history intact; generic list queries filter it out by default.
Per-year numbering
Documents carry a number and a year, with prefixes from Settings, which is how most small businesses already number invoices.

06Performance

Keeping it fast

Paginated, indexed lists
Every list endpoint paginates on the server and sorts on indexed fields, so tables stay fast as customers and invoices grow.
Lean reads
List queries return plain objects and populate only the fields a row shows, instead of hydrating full Mongoose documents.
Summaries in the database
Dashboard totals come from aggregation pipelines rather than loading every invoice into Node and summing there.
One origin, one service
Express serves the Vite build, so there is no CORS round trip and one Render service is the whole deploy.

07Outcome

Where it landed

  • The full quote → invoice → payment loop, with PDFs and email.
  • A generic backend where a new entity is a model plus overrides.
  • Deployable in one container, seeded with a single command.
The generic CRUD layer paid for itself by the third module, but the money logic is exactly where it should not be generic — keeping totals and payment status in explicit, server-side code is what made the numbers trustworthy.
Next case studyPEN Ticketing System