The easiest way to see what I am talking about is to click around a live store.

https://demo-ecommerce.rivil.app/en

It is an Astro storefront on our shop API. The brand, Casa Lumen, is a demo. You can browse the catalog, add items to the cart, use the wishlist, and go through checkout without creating an account. On the account page there is a prepared demo login if you do not want to register.

I am Paul. I build RIVIL (https://rivil.app) with a small team in Romania. I do the engineering. This is a write-up of how the product took its shape, and of the architectural choice I still think about the most: one codebase, and a database per client.

It did not start as an ERP.

A colleague made very good reports in Google Sheets and had no clean way to send them to clients. That is a small problem when you write it down, and a large one when it is the weekly rhythm of the business. I moved our existing clients onto a CRM so those reports could be produced and shared from one place.

The CRM grew in the direction the work was already going. We added a WordPress tracking plugin, with attribution paths, so we could see how a lead had actually arrived. Then we integrated with the old systems those clients were already running. I will not name the products. What matters is what the integrations exposed: the same gaps, again and again, in systems that had been forked, patched, and then left behind.

That is why I built the ERP, on the same design as the CRM, and why I made it API-first. If the data and the workflows already live in one backend, a website, a mobile app, or a store should be able to use them. They should not be a second product with a second database and a second idea of what a customer or an order is.

The architecture, said simply, is this. The real infrastructure is more complex, but the rule is not:

There is one codebase.
Features are split into modules and enabled per client.
Each client has their own database.
We add features to the shared codebase instead of maintaining a rewritten fork for every install. With a traditional ERP, 100 clients often means 100 codebases. Those copies barely get updated, because every upgrade is a custom project. I did not want to run the company that way. If we improve checkout, or stock handling, or the way a storefront reads the catalog, that work should be able to reach the clients who are already on the system, not only the next deal.

A separate database per client is the trade I chose in the other direction. It is more operational work than a single shared tenant table with a client_id column. Migrations have to be applied many times, not once. I still prefer it. Client data stays isolated by default. A heavy client does not sit on a neighbour's row lock. When something goes wrong, you are looking at one tenant's database, not a shared pile. Modules stay in the same codebase; turning one on for a client is a configuration choice, not a new branch that slowly diverges.

The shop API is one surface of that backend. The public shape is a versioned HTTP API under a per-store Bearer key. In outline:

Base: https://erp.rivil.app/api/shop/v1/
Catalog: GET /products
Cart: POST /carts, then lines and discounts on /carts/{token}
Checkout: POST /carts/{token}/checkout, which creates the order in the ERP and decrements stock
The Astro app does not call that API from the browser as a raw third-party client. It uses same-origin routes, /api/cart, /api/search, and /api/wishlist, in front of it. Checkout is described as idempotent, so a retried request should not create a second order. We also describe signed webhooks for changes. Documentation is available on request, or to partners. It is not on a public docs host today, on purpose.

I care about the storefront being fast enough that the API work is not wasted. I ran PageSpeed Insights on the homepage and got 92 on mobile and 99 on desktop. I also timed the cart API with TLS already up, from a machine that is not in Romania, without placing an order. Median GET /api/cart was 131ms. Median POST to add a line was 201ms. Median GET /api/search was 168ms. Those numbers include the network, not just application time.

I am not claiming this is the only way to build an ERP, or that multi-tenant-with-one-database is wrong. I am saying this is the model I could stand behind after watching forked installs go stale. The demo is there so you can judge the storefront and the API shape yourself, not so I can tell you it is fast.

If you have a minute with it, the feedback I would like is specific. If you were wiring your own frontend to this API, what is missing or badly named? If you have operated a single codebase with a database per customer, what broke first for you: migrations, module boundaries, reporting, or something I have not hit yet? And if you go through the cart or the account flow, what feels awkward?

The demo is here:

https://demo-ecommerce.rivil.app/en

More context on the headless side is here:

https://rivil.app/en/headless/ecommerce

Thanks for reading.