EN: How we built a POS system based on the Medusa.js framework
Today, I’m taking you into the world of Medusa.js by describing how we built a POS extension for handling vending machines for our client.
Hi!
This is the first article, but definitely not the last one, about headless solutions for online stores and websites.
What is Medusa.js?
Medusa.js is a framework for building headless online stores, where the back-end is based on Node.js and TypeScript. The front-end, can be built with anything. By default, Medusa.js supports Next.js, but you can easily choose Astro or Nuxt.js instead.
Why did we choose Medusa.js?
We chose this framework for several reasons.
The first one is that it is, and will remain, open source, while also offering ready-to-use modules from the start.
The Medusa.js team is doing a great job!
The second reason is that it is a headless solution, which means the back-end and front-end work together independently through a REST API.
The third reason is that the framework is based on Node.js, fully typed, and designed with modularity and flexibility in mind. This was important for our team because we work with technologies based on Node.js, React.js, and Next.js on a daily basis.
What was the reason behind the project?
The need was to build an online store where users could shop online while also connecting the shopping experience with the offline world.
To support vending machines, we needed a flexible and efficient solution. Modularity was also important, as it allowed us to:
manage multiple sales channels, where one sales channel corresponds to exactly one location,
connect a selected payment method,
display products available in the store before purchase,
show users their previously purchased products in their account after logging in, whether they bought them online or offline.
Architecture
The vending machine has three modes: purchase, return, and service.
Purchase mode - the customer can buy a product from the fridge by taking it from a specific scale.
Return mode - the customer can return an empty product container by placing it back on the scale.
Service mode - the administrator can open the device and, for example, restock it, assign the right products to a specific scale or synchronize products.
Additional technical solutions
Additional solutions we implemented:
Firestore - between the user and the back-end for fast data exchange,
MQTT - between the machine and the back-end, we used a lightweight communication protocol often used in IoT systems,
Stripe - a system for handling payments.
What features did we implement?
Our solution included several interesting features:
Sessions - we needed a single source of truth for sessions.
When we open the device, we need to communicate with it asynchronously.
In the database, we store data such as:session ID,
device ID,
hub ID,
locker ID,
session type,
current code,
the mode in which the device was opened,
user ID.
Session preview - the administrator can view all currently created sessions. If something happened and the device did not send us information about being closed, we can check it here and analyze the situation.
Turning service mode on and off - the administrator can open the device and organize it or replace products.
Machine configuration - we added the option to add new devices and assign them to a given location. Thanks to this, one back end can handle multiple vending machines from a single place.
Product assignment - products are assigned to a specific scale, and there can be many of them. Apart from basic data, each product also contains information such as the minimum and maximum product weight.
Synchronization with the device - the source of truth for products is the product management module in Medusa.js. After assigning products to scales, we can synchronize them with the device using a single button.
Notifications - using our automation and notification plugin, we implemented custom notifications that inform administrators about the state of the device.
For example, when a device goes offline, the administrator receives a notification in Slack that something is happening with it. This allows them to react very quickly.
What did we learn?
The most important lesson is that you need to test. And then test again.
Machines can be unreliable, and you never know what might happen. Together with the team responsible for the software on the hardware side, we faced countless edge cases. We had to predict different behaviors of the user, the software, and the hardware.
More than once, the doors were supposed to open, but then… nothing. The signal was sent, but the software on the hardware side did not detect it. Or it detected it, but the update failed.
Another example was when the device sent us information that the doors were closed, but a simple bug appeared on the back-end side, and the session did not close correctly. The same happened the other way around: for some reason, the doors did not close, and the session could remain open.
Another challenge was the question: what happens if someone bumps into the machine? Will the signal be sent or not?
It is hard to count how many attempts we made when calibrating and assigning weights to the scales.
Summary
This was a very interesting challenge for us because we had not previously worked on projects where communication with IoT systems was required. It is a completely different approach, because here we could not rely on synchronous responses in the same way as in standard web solutions.
Medusa.js is a very flexible and modular solution based on modern technology. It offers a lot of possibilities while maintaining quality and performance.
I hope you enjoyed the article and learned something new about the possibilities this framework offers, as well as how we approached solving these problems.


