<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Paul Iordache</title>
    <description>The latest articles on DEV Community by Paul Iordache (@paul_iordache_f603b0a2040).</description>
    <link>https://dev.to/paul_iordache_f603b0a2040</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4244026%2F78c25cf2-2685-485d-adcb-058c74f78c5f.png</url>
      <title>DEV Community: Paul Iordache</title>
      <link>https://dev.to/paul_iordache_f603b0a2040</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paul_iordache_f603b0a2040"/>
    <language>en</language>
    <item>
      <title>Building a headless ERP: one codebase, a database per client</title>
      <dc:creator>Paul Iordache</dc:creator>
      <pubDate>Sat, 14 Nov 2026 06:53:23 +0000</pubDate>
      <link>https://dev.to/paul_iordache_f603b0a2040/building-a-headless-erp-one-codebase-a-database-per-client-1932</link>
      <guid>https://dev.to/paul_iordache_f603b0a2040/building-a-headless-erp-one-codebase-a-database-per-client-1932</guid>
      <description>&lt;p&gt;The easiest way to see what I am talking about is to click around a live store.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo-ecommerce.rivil.app/en" rel="noopener noreferrer"&gt;https://demo-ecommerce.rivil.app/en&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I am Paul. I build RIVIL (&lt;a href="https://rivil.app" rel="noopener noreferrer"&gt;https://rivil.app&lt;/a&gt;) 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.&lt;/p&gt;

&lt;p&gt;It did not start as an ERP.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The architecture, said simply, is this. The real infrastructure is more complex, but the rule is not:&lt;/p&gt;

&lt;p&gt;There is one codebase.&lt;br&gt;
Features are split into modules and enabled per client.&lt;br&gt;
Each client has their own database.&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;Base: &lt;a href="https://erp.rivil.app/api/shop/v1/" rel="noopener noreferrer"&gt;https://erp.rivil.app/api/shop/v1/&lt;/a&gt;&lt;br&gt;
Catalog: GET /products&lt;br&gt;
Cart: POST /carts, then lines and discounts on /carts/{token}&lt;br&gt;
Checkout: POST /carts/{token}/checkout, which creates the order in the ERP and decrements stock&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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?&lt;/p&gt;

&lt;p&gt;The demo is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo-ecommerce.rivil.app/en" rel="noopener noreferrer"&gt;https://demo-ecommerce.rivil.app/en&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;More context on the headless side is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://rivil.app/en/headless/ecommerce" rel="noopener noreferrer"&gt;https://rivil.app/en/headless/ecommerce&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>database</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
