<?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: Pavel L.</title>
    <description>The latest articles on DEV Community by Pavel L. (@kenzap).</description>
    <link>https://dev.to/kenzap</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3822171%2F19112100-655b-4bf2-938f-67520c03e1fd.jpg</url>
      <title>DEV Community: Pavel L.</title>
      <link>https://dev.to/kenzap</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kenzap"/>
    <language>en</language>
    <item>
      <title>5 Reasons Why I Built an ERP in Vanilla JavaScript</title>
      <dc:creator>Pavel L.</dc:creator>
      <pubDate>Sat, 04 Apr 2026 09:11:19 +0000</pubDate>
      <link>https://dev.to/kenzap/5-reasons-why-i-built-an-erp-in-vanilla-javascript-1g87</link>
      <guid>https://dev.to/kenzap/5-reasons-why-i-built-an-erp-in-vanilla-javascript-1g87</guid>
      <description>&lt;p&gt;&lt;em&gt;"What framework did you use?"&lt;/em&gt; I hear this quite often from other devs when I share my journey building the ERP system I completed this year.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Freedom
&lt;/h2&gt;

&lt;p&gt;Every time I started building with Svelte, React or Next.js I felt...  constrained. I don't want to criticise these technologies, they are great and each has its audience. Back in the day they solved real limitations of JavaScript around code organisation, modularity, component architecture and state management.&lt;/p&gt;

&lt;p&gt;But today things are different. JavaScript itself has grown up. You can build reusable components, use ES module imports natively, write clean arrow functions and structure things in an object oriented way without a framework telling you how.&lt;/p&gt;

&lt;p&gt;There is one catch though. JavaScript is not a framework. No enforced structure, no constraints, absolute freedom to build things exactly the way you want. Including doing them completely wrong. That is what most developers associate with the word "Vanilla" and honestly I get it.&lt;/p&gt;

&lt;p&gt;Here is a snippet from the dashboard where I manage ERP users:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7w590xrplpcvlix0jz0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7w590xrplpcvlix0jz0.png" alt="User Page Class declaration in the ERP" width="800" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kenzap/factory/blob/main/public/users/index.js" rel="noopener noreferrer"&gt;Full Class Preview&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few things worth noting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Many imports.&lt;/strong&gt; Helps with modularity. The cool thing is this runs directly in the browser without compilation, though I would not recommend skipping it in production. More on that below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Classes.&lt;/strong&gt; I use them mainly as entry points for each page of the ERP while keeping everything else in exportable modules outside. No deep reason for classes here really, I just find the readability better and the patterns I repeat across pages help me navigate the codebase faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Repeatable pattern.&lt;/strong&gt; Every page class follows the same structure:&lt;br&gt;
&lt;br&gt;
 &lt;code&gt;jsconstructor → init → html → render → listeners&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;Works for Users, Manufacturing Journal, Orders, everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Universal components&lt;/strong&gt; like &lt;code&gt;Locale&lt;/code&gt;, &lt;code&gt;Session&lt;/code&gt;, &lt;code&gt;Header&lt;/code&gt; and &lt;code&gt;Footer&lt;/code&gt; shared across the whole system.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Aesthetics and Minimalism
&lt;/h2&gt;

&lt;p&gt;This matters less now that AI generates so much code but still. The freedom JavaScript gives you to structure things however you want makes it easy to keep things clean.&lt;/p&gt;

&lt;p&gt;Code should look good. Bad looking code belongs in bad projects.&lt;/p&gt;

&lt;p&gt;Simple repeatable methods like &lt;code&gt;init&lt;/code&gt; and &lt;code&gt;html&lt;/code&gt;, shared classes like &lt;code&gt;Header&lt;/code&gt; and &lt;code&gt;Footer&lt;/code&gt;, make the codebase easy to read without any bloat or unnecessary dependencies weighing things down.&lt;/p&gt;

&lt;p&gt;I skip types. Simple variables and objects passed through methods or event bus handlers. I know the argument, there is no reason not to use types, but I don't care as long as everything works and the code stays concise. For a solo built system where I know every data structure by heart this works fine. I would not necessarily recommend this to a larger team.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Full Stack JavaScript
&lt;/h2&gt;

&lt;p&gt;If I broke down my programming time by language it would go: PHP, JavaScript, Java, Python. Of those JavaScript and Python appeal to me most. Python's conciseness is great but the indentation style is not for me.&lt;/p&gt;

&lt;p&gt;Node.js is pure JavaScript on the backend. When I was working with Next.js I kept asking myself: wait, is this frontend or backend? I think beginners run into this too. At first I thought I was just inexperienced but I now think it is a structural issue. With a different backend language the boundary is obvious immediately.&lt;/p&gt;

&lt;p&gt;The biggest practical advantage of JavaScript on both ends is sharing identical logic. In my ERP I calculate various VAT rates across different scenarios. Things get complicated fast in the EU depending on whether your company is VAT registered and whether goods fall under a special tax regime.&lt;/p&gt;

&lt;p&gt;I created a &lt;code&gt;packages&lt;/code&gt; folder at the root of the project that shares the exact same tax calculation code across PDF invoices, accounting reports and live frontend quotation previews:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiacdj2n6r48l6fpflip5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiacdj2n6r48l6fpflip5.png" alt="packages folder structure" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One source of truth for something this critical is not optional.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Performance
&lt;/h2&gt;

&lt;p&gt;React carries 100KB+ before anything else loads. JavaScript runs natively in the browser. Everything else compiles down to it first. JavaScript just runs, straight from the start.&lt;/p&gt;

&lt;p&gt;With a single JS file I can fetch everything needed for full UI rendering and localisation in one shot. The moment the script loads it executes.&lt;/p&gt;

&lt;p&gt;Here are the Lighthouse scores. Most pages hit 100% without a single optimisation pass:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop8x0uah231x4h1jq4sz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fop8x0uah231x4h1jq4sz.png" alt="Lighthouse 100% performance score" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some pages drop to around 80% but that is mainly due to large datasets and servers sitting thousands of kilometres away, not the architecture.&lt;/p&gt;

&lt;p&gt;For the inventory page above the entire thing is one JS file at 30KB. I do use Bootstrap which adds 81KB of JS on top, that is a deliberate trade for UI consistency and something I have not optimised away yet. But with 100% Lighthouse scores across most pages the question becomes whether there is anything left worth optimising.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. No Dependency Hell
&lt;/h2&gt;

&lt;p&gt;How many times have you been there. A package needs updating because it is outdated or worse vulnerable. You run the update, get lucky, and then something breaks somewhere completely unrelated.&lt;/p&gt;

&lt;p&gt;Basic projects end up with hundreds of dependencies. Now imagine an ERP with 20+ journals, reports and manufacturing modules all tightly coupled. When something breaks you are stuck.&lt;/p&gt;

&lt;p&gt;I am not saying I avoid dependencies entirely. Three.js is a good example of one I rely on. But overall the project is much cleaner for it. My &lt;code&gt;node_modules&lt;/code&gt; mostly feeds the Node backend, not the frontend.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI
&lt;/h2&gt;

&lt;p&gt;Is there an article that does not mention AI today. So here we go.&lt;/p&gt;

&lt;p&gt;Something I keep thinking about is how ERP software updates should actually work in the future. The traditional model is one shared codebase, customisations live in their own layer, new releases keep everyone on the same version. It works but it is rigid.&lt;/p&gt;

&lt;p&gt;What if the model flipped. Shipping code is cheaper and faster than it has ever been. What if every client just gets their own codebase from day one. No modifying a core that affects everyone. No releases nobody asked for. Each installation just evolves alongside the company using it.&lt;/p&gt;

&lt;p&gt;This alone deserves a separate article but the part that connects to vanilla JS is this. I want to build an environment where AI agents can navigate the codebase, understand it and make changes directly based on user feedback. Not in staging. In production.&lt;/p&gt;

&lt;p&gt;I know that sounds alarming. But imagine a user complains about an awkward button. Two minutes later it is a toggle instead of a checkbox and everyone moves on. No ticket, no sprint, no deployment pipeline.&lt;/p&gt;

&lt;p&gt;The decoupled way my ERP pages are structured, and the fact that almost every part can be overridden through an overrides folder, gives me a starting foundation where AI generated changes are contained, easy to test visually and easy to revert without recompiling anything or restarting a container.&lt;/p&gt;

&lt;p&gt;No compilation means you drop the code and check it in the browser immediately. I genuinely want to know if there is a workflow in React or Next.js that gets close to this.&lt;/p&gt;




&lt;h2&gt;
  
  
  Things I Learned Along the Way
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Document title timing
&lt;/h3&gt;

&lt;p&gt;All my data fetching is packed into a single JS call after page load. This works fast but it means I set the document title after the data arrives since that lives in &lt;code&gt;index.html&lt;/code&gt;. Minor but worth knowing going in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decoupled pages
&lt;/h3&gt;

&lt;p&gt;I moved toward treating each section of the UI as its own isolated thing. No centralised routing, no injected scripts that may not be needed on every page. The product editing page is heavy, 3D declarations, image uploads, an &lt;code&gt;.obj&lt;/code&gt; viewer with Three.js. Keeping it isolated means that complexity stays contained.&lt;/p&gt;

&lt;h3&gt;
  
  
  State management
&lt;/h3&gt;

&lt;p&gt;This is the one thing I wish I had done better. I built helper functions like &lt;code&gt;onClick&lt;/code&gt; and &lt;code&gt;onChange&lt;/code&gt; because writing &lt;code&gt;document.addEventListener&lt;/code&gt; everywhere felt unnecessarily verbose. They work well until the frontend grows. Then partial DOM updates cause some listeners to go missing while others fire twice.&lt;/p&gt;

&lt;p&gt;This means I end up solving problems React and Svelte have already solved. React through the virtual DOM, Svelte by manipulating the DOM directly.&lt;/p&gt;

&lt;p&gt;The fix that actually worked was declaring listeners directly in the HTML. It sounds old school but it holds up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;That element always has the same reference no matter what happens to surrounding code. No separate initialisation needed.&lt;/li&gt;
&lt;li&gt;You can see exactly what is listening right where the element lives.&lt;/li&gt;
&lt;li&gt;No double executions, no mystery triggers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bundling
&lt;/h3&gt;

&lt;p&gt;Running without compilation with many ES module imports causes browser caching issues that stick around even in incognito. A simple Rollup script fixed the live reloading and the caching disappeared. Loading 20+ individual imports is not ideal in production but the fact that something as complex as an ERP can run this way, no bundles, no framework, no frontend compilation step, is still something I find genuinely interesting.&lt;/p&gt;

&lt;p&gt;Maybe it is the future. Who knows.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>node</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Architecture of an Open Source Manufacturing ERP</title>
      <dc:creator>Pavel L.</dc:creator>
      <pubDate>Wed, 18 Mar 2026 08:14:49 +0000</pubDate>
      <link>https://dev.to/kenzap/architecture-of-an-open-source-manufacturing-erp-161g</link>
      <guid>https://dev.to/kenzap/architecture-of-an-open-source-manufacturing-erp-161g</guid>
      <description>&lt;p&gt;This software’s first version dates back to the 1990s, long before GitHub existed.&lt;/p&gt;

&lt;p&gt;Technologies and capabilities were very different then, but some of those UI/UX ideas were strong enough to survive multiple major revamps, including a full rebuild from scratch this year.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kenzap" rel="noopener noreferrer"&gt;
        kenzap
      &lt;/a&gt; / &lt;a href="https://github.com/kenzap/factory" rel="noopener noreferrer"&gt;
        factory
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Open-source Manufacturing ERP / Factory OS
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Kenzap Factory&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://raw.githubusercontent.com/kenzap/factory/main/preview.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkenzap%2Ffactory%2Fmain%2Fpreview.png" alt="Factory"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;About&lt;/strong&gt;: Manufacturing resource planning and quotation software. Production, costing, estimation and planning designed for job-based manufacturing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;ES6 JavaScript&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://kenzap.com" rel="nofollow noopener noreferrer"&gt;https://kenzap.com&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What is "Kenzap Factory"?&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;An ERP system designed specifically for metal fabricators. It supports real production workflows. The focus is on job shops, make-to-order production, and compliance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manufacturing journal&lt;/li&gt;
&lt;li&gt;Client journal&lt;/li&gt;
&lt;li&gt;Access and user management&lt;/li&gt;
&lt;li&gt;Metal cutting and nesting integration&lt;/li&gt;
&lt;li&gt;Warehouse and product inventory&lt;/li&gt;
&lt;li&gt;Financial reports&lt;/li&gt;
&lt;li&gt;Analytics module&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Production Workflow Support:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Job-based manufacturing processes&lt;/li&gt;
&lt;li&gt;Real-time production tracking&lt;/li&gt;
&lt;li&gt;Cost calculation and planning&lt;/li&gt;
&lt;li&gt;Compliance management&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;User Journey&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Below is a comprehensive overview of user workflows within this ERP system. Items marked with "-" are currently in development.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;General User&lt;/h3&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Securely log in to the platform using WhatsApp or Email (2FA) ✓&lt;/li&gt;
&lt;li&gt;Change platform language ✓&lt;/li&gt;
&lt;li&gt;Navigate seamlessly across the platform ✓&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Manager&lt;/h3&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Create new orders ✓&lt;/li&gt;
&lt;li&gt;Access order logs ✓&lt;/li&gt;
&lt;li&gt;Manage client records…&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kenzap/factory" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;This ERP is built around real factory workflows, where quotations, production execution, material write-off, and financial reconciliation all happen within one operational loop. &lt;/p&gt;

&lt;p&gt;My goal is to create software that meets all modern factory needs, is technologically fresh, and provides unlimited room for AI innovation and automation synergies.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Architecture Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0i1ywmqszwh2lfr74fbc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0i1ywmqszwh2lfr74fbc.png" alt="File structure of Kenzap Factory"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is a &lt;strong&gt;Node.js + Express backend&lt;/strong&gt; with an &lt;strong&gt;ES module frontend&lt;/strong&gt;, built and served as one deployable application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core directories
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;server/&lt;/code&gt;&lt;/strong&gt;: APIs, documents, storage gateway, extension loading
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;public/&lt;/code&gt;&lt;/strong&gt;: frontends (orders, manufacturing, worklog, cutting, stock, settings, etc.)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;packages/&lt;/code&gt;&lt;/strong&gt;: shared runtime logic across frontend/backend (for example, &lt;code&gt;tax-core&lt;/code&gt;)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;overrides/&lt;/code&gt;&lt;/strong&gt;: tenant-specific replacements without touching core files
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;server/extensions/&lt;/code&gt;&lt;/strong&gt;: plugin-like runtime extensions
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dist/&lt;/code&gt;&lt;/strong&gt;: compiled production output of &lt;code&gt;public/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docs/&lt;/code&gt;&lt;/strong&gt;: documentation and AI-assisting files for faster development&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Build model
&lt;/h3&gt;

&lt;p&gt;Frontend compilation is handled with Rollup, but it can also run without compilation because modern browsers support ES modules natively.&lt;/p&gt;

&lt;p&gt;There is no heavy frontend framework involved. I borrowed a few structural ideas from Next.js, but the codebase is intentionally framework-free and modular.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;JavaScript is now mature enough to handle large-scale architecture cleanly without mandatory framework coupling.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  2. Data
&lt;/h2&gt;

&lt;p&gt;All business data is stored in PostgreSQL (except uploaded files).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PostgreSQL gives us relational database standards, strong JSONB support, and vector embedding support in one place.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The system uses two tables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;data&lt;/code&gt;&lt;/strong&gt;: JSONB payload table for core ERP entities
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;metering&lt;/code&gt;&lt;/strong&gt;: timestamp-based machine/factory readings
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2.1. &lt;code&gt;data&lt;/code&gt; table
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqn9drrcsyo828qgr199h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqn9drrcsyo828qgr199h.png" alt="overview of a data table structure in Adminer of Kenzap Factory"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core columns&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_id&lt;/code&gt;&lt;/strong&gt; — unique 40-character record ID
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pid&lt;/code&gt;&lt;/strong&gt; — parent relationship ID
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sid&lt;/code&gt;&lt;/strong&gt; — tenant/space ID (for SaaS mode)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ref&lt;/code&gt;&lt;/strong&gt; — logical entity type (&lt;code&gt;order&lt;/code&gt;, &lt;code&gt;product&lt;/code&gt;, &lt;code&gt;entity&lt;/code&gt;, &lt;code&gt;settings&lt;/code&gt;, etc.)
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;js&lt;/code&gt;&lt;/strong&gt; — full JSONB payload
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;embedding&lt;/code&gt;&lt;/strong&gt; — optional vector field for AI/agentic operations
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach is opinionated. It needs query discipline and indexing strategy, but it enables very fast iteration for highly dynamic domain models.&lt;/p&gt;

&lt;p&gt;Below is an example of an order stored with &lt;code&gt;ref = order&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4hotculj6t40mwqu1r7p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4hotculj6t40mwqu1r7p.png" alt="Order JSON data structure example of Kenzap Factory"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  2.2. &lt;code&gt;metering&lt;/code&gt; table
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxgcxise8likvwmitr6mz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxgcxise8likvwmitr6mz.png" alt="overview of a metering table structure in Adminer of Kenzap Factory"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core columns&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;_id&lt;/code&gt;&lt;/strong&gt; — unique record ID
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;time&lt;/code&gt;&lt;/strong&gt; — UTC timestamp
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reading&lt;/code&gt;&lt;/strong&gt; — numeric reading
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;machine&lt;/code&gt;&lt;/strong&gt; — machine ID
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;type&lt;/code&gt;&lt;/strong&gt; — reading type
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In manufacturing environments, this data enables predictive maintenance, heat maps, load balancing, and scheduling optimizations.&lt;/p&gt;




&lt;h3&gt;
  
  
  2.3. File storage
&lt;/h3&gt;

&lt;p&gt;Product images, 3D models, and other assets are stored in S3-compatible buckets.&lt;/p&gt;

&lt;p&gt;For self-hosting, you can use SeaweedFS or any cloud object storage provider.&lt;/p&gt;

&lt;p&gt;Storage is abstracted through &lt;strong&gt;&lt;code&gt;createStorageProvider&lt;/code&gt;&lt;/strong&gt;, so the infrastructure layer stays portable.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Business Workflow as Architecture
&lt;/h2&gt;

&lt;p&gt;The architecture follows this factory loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quotations&lt;/strong&gt;: manager creates quotation/order with formula-based pricing
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production&lt;/strong&gt;: team executes stages in manufacturing journal + logs work in worklog
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Materials&lt;/strong&gt;: cutting and metal log manage coil usage and write-offs
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documents&lt;/strong&gt;: invoices, waybills, production slips generated server-side
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finance&lt;/strong&gt;: payments, balances, accounting exports
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reporting&lt;/strong&gt;: manufacturing output, employee performance, machine activity
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forecasting&lt;/strong&gt;: AI-driven planning and KPI analytics (currently early stage)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Frontend Organization
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F65n211ibytsh0dk4glyd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F65n211ibytsh0dk4glyd.png" alt="Frontend Organisation of Kenzap Factory"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The UI is organized as operational journals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;public/orders&lt;/code&gt;, &lt;code&gt;public/order&lt;/code&gt;, &lt;code&gt;public/manufacturing&lt;/code&gt;, &lt;code&gt;public/worklog&lt;/code&gt;, &lt;code&gt;public/cutting&lt;/code&gt;, &lt;code&gt;public/payments&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Shared logic in &lt;code&gt;public/_/components&lt;/code&gt;, &lt;code&gt;public/_/modules&lt;/code&gt;, &lt;code&gt;public/_/helpers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Action-heavy, modal-heavy UX optimized for fast factory-floor execution&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Backend Organization
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1d94lurdo2mw8f16l99.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff1d94lurdo2mw8f16l99.png" alt="Backend Organisation of Kenzap Factory"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;server/_&lt;/code&gt;&lt;/strong&gt; — helpers, dependencies, reusable backend components
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;server/api&lt;/code&gt;&lt;/strong&gt; — API routes
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;server/document&lt;/code&gt;&lt;/strong&gt; — PDF and export generators
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;server/extensions&lt;/code&gt;&lt;/strong&gt; — extension runtime
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;server/scripts&lt;/code&gt;&lt;/strong&gt; — migration and ops scripts
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;server/server.js&lt;/code&gt;&lt;/strong&gt; — Express entry point
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Route strategy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Core routes are loaded from &lt;code&gt;server/api/*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;In practice, tenant-specific behavior should go to &lt;code&gt;overrides/server/api/*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Same approach for document generation via &lt;code&gt;server/document/*&lt;/code&gt; and overrides&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Extension model
&lt;/h3&gt;

&lt;p&gt;Extensions in &lt;code&gt;server/extensions/*&lt;/code&gt; are loosely coupled feature modules.&lt;br&gt;&lt;br&gt;
Each extension has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;index.js&lt;/code&gt; (entry point)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;manifest.json&lt;/code&gt; (metadata/capabilities/permissions)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  6. Dynamic Settings
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi5nk308pe7w37yy6uooy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi5nk308pe7w37yy6uooy.png" alt="Kenzap Factory ERP settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Critical configuration is editable directly from the dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;brand name, VAT number, tax region&lt;/li&gt;
&lt;li&gt;timezone (affects cron + generated PDFs)&lt;/li&gt;
&lt;li&gt;invoice/email/WhatsApp templates&lt;/li&gt;
&lt;li&gt;system variables and attributes&lt;/li&gt;
&lt;li&gt;inventory and raw material pricing&lt;/li&gt;
&lt;li&gt;worklog and stock categories&lt;/li&gt;
&lt;li&gt;extension-specific settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Settings are cached in memory for low-latency access.&lt;/p&gt;


&lt;h2&gt;
  
  
  7. Localization
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5fss02hsursffsk467t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5fss02hsursffsk467t.png" alt="Home dashboard of Kenzap Factory in Latvian language"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every text in the system is localizable (frontend + backend).&lt;/p&gt;
&lt;h3&gt;
  
  
  Localization helpers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;__html('Your text')&lt;/code&gt; — safe translation for HTML content
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__attr('Your text')&lt;/code&gt; — safe translation for HTML attributes
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__('Your text')&lt;/code&gt; — plain translated string
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Tooling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm run locales:export&lt;/code&gt; — extract localizable strings
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm run locales:translate:lv&lt;/code&gt; — auto-translate missing strings (for example, Latvian)
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The translation script uses Anthropic API (&lt;code&gt;server/scripts/locales-translate.js&lt;/code&gt;).&lt;/p&gt;


&lt;h2&gt;
  
  
  8. Upcoming Releases
&lt;/h2&gt;

&lt;p&gt;Since the beginning of this year, at least one production feature has shipped almost every week.&lt;/p&gt;

&lt;p&gt;You’re welcome to share feedback and ideas.&lt;/p&gt;

&lt;p&gt;Planned work is tracked here:&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/kenzap/factory/tree/main/docs/exec-plans/active" rel="noopener noreferrer"&gt;https://github.com/kenzap/factory/tree/main/docs/exec-plans/active&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  9. License
&lt;/h2&gt;

&lt;p&gt;This project is licensed under &lt;strong&gt;Apache-2.0&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
It is friendly for commercial use and does not require publishing your modified source code.&lt;/p&gt;
&lt;h2&gt;
  
  
  10. Overview
&lt;/h2&gt;

&lt;p&gt;For a more visual overview of the system check this video below.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/B1dwccjsf_M"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

</description>
      <category>manufacturing</category>
      <category>erp</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Machine Telemetry and Factory Metering API</title>
      <dc:creator>Pavel L.</dc:creator>
      <pubDate>Fri, 13 Mar 2026 14:00:20 +0000</pubDate>
      <link>https://dev.to/kenzap/machine-telemetry-and-factory-metering-api-1dod</link>
      <guid>https://dev.to/kenzap/machine-telemetry-and-factory-metering-api-1dod</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgxxmbptgnsaiufjn18h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqgxxmbptgnsaiufjn18h.png" alt="The UI of a metal bending machine used for telemetry" width="800" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Factory machines are a silent source of valuable insights. By collecting just a single metric from different machines, it is possible to visualize the load of the entire factory by hour, identify when shifts start and end, and detect the busiest workstations.&lt;/p&gt;

&lt;p&gt;So I decided to create this guide to show how I collect readings from a bending machine UI and store them in a PostgreSQL table.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Goal
&lt;/h2&gt;

&lt;p&gt;Capture changes in the machine’s bend counter and store them as time-series records. The goal is to build useful telemetry inside the factory ERP.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Structure
&lt;/h2&gt;

&lt;p&gt;I defined a PostgreSQL table structure suitable for storing timestamp-based readings. An example is shown below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl87dni4n0nz8ib1kz78l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl87dni4n0nz8ib1kz78l.png" alt="Adminer dashboard with metering schema defined" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screenshot is taken from Adminer, which I use as a web interface to interact with my PostgreSQL database. I find it great for basic tasks, although there are probably better alternatives.&lt;/p&gt;

&lt;p&gt;Next, I created a separate folder that works as an extension inside my ERP system. This is where the API endpoints reside. Its purpose is to accept POST requests:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;POST /extension/machine-tracking/store-reading&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then I defined the payload (Codex actually helped generate it):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"machine_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BM301"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reading"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-13T10:30:00.000Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are three main variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the machine ID&lt;/li&gt;
&lt;li&gt;the reading value&lt;/li&gt;
&lt;li&gt;the timestamp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The machine I started this integration with is designed to bend metal, as shown below.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fppodqkgjwxonbrql8zsa.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fppodqkgjwxonbrql8zsa.gif" alt="Bending beam of the machine" width="240" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;View from a side.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2j1lndyg19bpp48mmkmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2j1lndyg19bpp48mmkmu.png" alt="A machine bending a sheet of metal" width="800" height="723"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Technically, there are many metrics that could be captured, such as the total number of cycles, power on/off signals, and others. However, simplifying the integration to a single parameter does not necessarily reduce the quality of telemetry.&lt;/p&gt;

&lt;p&gt;So I decided to track just one metric, which is the number of bends.  Specifically the number of times the metal beam moves up and down.&lt;/p&gt;

&lt;p&gt;Each time the value changes, an API request is triggered and the backend stores the reading. Once multiple readings are collected over time, charts can be generated to visualize how busy the machine is throughout the day.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Building API
&lt;/h2&gt;

&lt;p&gt;Well, prompts again. I sent the following prompt to Codex.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4qlv6tftbqkl2j7ym84a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4qlv6tftbqkl2j7ym84a.png" alt="Screenshot from Codex prompt" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The schema called &lt;code&gt;metering&lt;/code&gt; to store reading counters from various machines on the factory floor was already created in the previous step.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The final output consisted of two main files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;index.js&lt;/strong&gt; — defines the API route&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;src/store.js&lt;/strong&gt; — handles storing the data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Below is a screenshot of &lt;code&gt;index.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe8yh5halh4g26t1d0f8e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe8yh5halh4g26t1d0f8e.png" alt="Schreenshot of express endpoint" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not much is happening here. We simply extend an Express route and perform basic validation of the incoming data before passing the payload to the &lt;code&gt;store&lt;/code&gt; method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const response = await store(req.body, db, logger);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;store.js&lt;/code&gt; file, we pass the three payload parameters to the PostgreSQL database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvh5s8dqk1c7wo4zq8e65.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvh5s8dqk1c7wo4zq8e65.png" alt="Postgres insert query of metering payload" width="800" height="268"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can browse these files directly in my repository:&lt;br&gt;
&lt;a href="https://github.com/kenzap/factory/tree/main/server/extensions/machine-tracking" rel="noopener noreferrer"&gt;telemetry extension&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Getting Data From the Machine
&lt;/h2&gt;

&lt;p&gt;Now that the API and database are set up, the next question is: &lt;strong&gt;where do we get the data from?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My first thought was to use sensors connected to a Raspberry Pi acting as a gateway that would forward the payload over the internet to my ERP server.&lt;/p&gt;

&lt;p&gt;However, since I understand how the machine’s UI operates (it’s a relatively simple browser interface communicating with the PLCs), I decided to tweak the interface itself to extract the reading without installing any additional hardware.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmihf5jnnpd7qnnizn5x8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmihf5jnnpd7qnnizn5x8.png" alt="Bending machine interface" width="800" height="781"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The bending machine’s software still relies on &lt;strong&gt;jQuery version 2.0&lt;/strong&gt; which was released over a decade ago.&lt;/p&gt;

&lt;p&gt;The only change I had to make after Codex generated the code was updating the endpoint. I changed it from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/extension/machine-tracking/store-reading
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://mydomain/extension/machine-tracking/store-reading
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;so that the machine interface could transmit the data to the ERP server publicly.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Debug
&lt;/h2&gt;

&lt;p&gt;Obviously, things did not work out of the box, but I felt I was very close. The first issue turned out to be the endpoint itself.&lt;/p&gt;

&lt;p&gt;It turned out that my default PostgreSQL user did not have permission to connect to the newly created &lt;code&gt;metering&lt;/code&gt; database.&lt;/p&gt;

&lt;p&gt;The fix was relatively simple. Using the Adminer interface, I extended the permissions for my default user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;skarda_design&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metering&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;skarda_design&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;USAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;SEQUENCE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metering__id_seq&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;skarda_design&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, I sent the request through Postman and it worked. I could hardly believe it, but when I checked the database, the payload had been stored correctly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6k6ekzbksw3r3ro4a1ny.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6k6ekzbksw3r3ro4a1ny.png" alt="Postman metering request example" width="800" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Even though I had deployed everything to production, the information was still not being transmitted from the machine. I opened the browser’s inspect panel on the machine interface and discovered that it was a &lt;strong&gt;CORS issue&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftksv1etidpmlh43blivj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftksv1etidpmlh43blivj.png" alt="Pavel Lukasenko, checking the code of the machine" width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I simply needed to configure Express to allow requests sent from &lt;code&gt;localhost&lt;/code&gt;. Once that was fixed and the browser tab on the machine was refreshed, the data started flowing.&lt;/p&gt;

&lt;p&gt;I then connected two more machines using unique &lt;code&gt;machine_id&lt;/code&gt; values and checked the &lt;code&gt;metering&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;The results speak for themselves.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxlq9c9bchg5wtmi517e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdxlq9c9bchg5wtmi517e.png" alt="Metered data stored in the database" width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Visualisation
&lt;/h2&gt;

&lt;p&gt;This is the most fascinating part, because we can now start thinking about meaningful insights from the collected data (I waited for 3 days before getting to this part) and how to visualize them. I defined three possible views:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shift heatmap&lt;/li&gt;
&lt;li&gt;Production rate trend&lt;/li&gt;
&lt;li&gt;Per-machine counter trend&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I sent another prompt. Here’s what we got.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz0232300oohw5bfx8p4p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz0232300oohw5bfx8p4p.png" alt=" " width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>manufacturing</category>
      <category>telemetry</category>
      <category>javascript</category>
      <category>postgressql</category>
    </item>
  </channel>
</rss>
