4 min read

How we generated multiple products UIs from a single codebase

How we generated *multiple products UIs* from a single codebase

I build a lot of ERPs at MyPartner ISC. One day I am writing code for a school, the next day it is for a retail shop, and the week after it is for a logistics company. The core logic feels identical most of the time, but every single client wants things done their own way.

For a long time, the company did what everyone does to move fast. Copy the folder, paste it into a new repository, and hope for the best. That worked fine for a few weeks, but then the updates started rolling in. Fixing a simple bug in one place meant manually copying that fix over to two other repositories. If I wanted to change a core component, I had to do it three times. It felt like running in place.

I knew I had to stop duplicating things before I lost my mind. I wanted to build everything inside a single codebase but still give every product its own unique look and feature set. When I pitched the idea of a config-driven UI architecture, everyone told me no. They thought it would be too complex or cause more breaking changes. I went ahead and executed it regardless.

I created one central repository for all the main modules like dashboards, sales, and student tracking. Then I built tiny host apps for each separate product. Everything connects through a simple configuration file.

export const PRODUCTS_CONFIG = {
  retailERP: {
    name: "Retail ERP",
    enabledModules: ["dashboard", "inventory", "sales"],
  },
  schoolERP: {
    name: "School ERP",
    enabledModules: ["dashboard", "students", "grades"],
  },
};

The host application reads this configuration file and pulls in exactly what it needs. To make this work smoothly, I made every single module responsible for its own paths. For example, the students module exports a simple array containing its own list of screens.

// modules/students/routes.ts
export const studentsRoutes = [
  { path: "/students", element: <StudentList /> },
];

Inside the main host app, the code looks at the active list of modules for that product. It maps them to the master list and uses a flatMap to combine all the paths together. The router handles the rest. This ensures the application only loads the code that is actually being used, keeping the final bundle small and clean.

const productKey = "schoolERP";
const enabledModules = PRODUCTS_CONFIG[productKey].enabledModules;

const MODULE_ROUTES = {
  dashboard: dashboardRoutes,
  inventory: inventoryRoutes,
  students: studentsRoutes,
  grades: gradesRoutes,
};

const routes = enabledModules.flatMap(
  (mod) => MODULE_ROUTES[mod] || []
);

People look at this setup and assume I went deep into complex microfrontends or module federation. I stayed away from that completely, even when others pushed for trendy tools. Heavy architecture brings runtime bugs and version issues that make daily work miserable. I chose a simple build-time isolation instead.

Every module lives in the same workspace with its own logic, hooks, and translation files. The host apps are completely empty shells. They only exist to hold the specific branding, colors, and the build setup. The shared modules stay organized in one place, and the hosts stay perfectly updated.

The change saved a massive amount of time. Instead of building the same layout multiple times for different apps, I write it once inside its module. If a new product needs it, I just add a string to the configuration array. Launching a new version for a new industry takes minutes now because I just spin up a tiny shell app and point it to the config.

When a rare case comes up where one client needs a slightly different UI inside a shared page, I handle it right at runtime. I use a custom hook to check which product is currently running, and I swap out the component conditionally.

const { currentProduct } = useProductConfig();

if (currentProduct.name === "School ERP") {
  return <SchoolSpecificComponent />;
}

You do not always need massive infrastructure or approval from everyone to solve a big problem. A single well-organized folder and a smart configuration file can give you exactly what you need without the headache.

Let's Keep in Touch

Subscribe and I'll send you updates on what I'm shipping, ideas I'm exploring, and probably too many side projects.

Rougher thoughts?

My unpolished notes on building products and learning new technologies.

Explore Notes
Written by Human