Fair warning: this one runs about 30 minutes and gets fairly deep into the weeds — architecture decisions, migrations, and the smaller problems that never make it into a changelog.
Writing like this is what I always imagined this blog would be — in the spirit of engineers like Nelson Elhage, whose posts I happily lose evenings to, even when I do not fully comprehend. It's the lore I'm chasing with debuglore: the honest, slightly messy record of how a system actually came together, written while my entrepreneurial life is as chaotic as ever.
Mostly this journal went through with AI help along the way, as everything today. Trimming, tightening, the usual. The work, the thinking, the tone, quick proof-reading, and the "why" are all mine. I don't have enough time to write such a long journal on my own and it would take weeks to come up. So here it is
By October 1, 2026, I had a deadline: October 15. There was no project plan, no negotiated delivery schedule, no specification waiting to be implemented — just a business situation that needed a response, a rough idea of what the product should become, and two weeks to turn that idea into something real.
The plan was mostly in my head. I knew what the business needed and had a reasonably good understanding of the systems we already had. What I didn't have was a finished design for how all of it should fit together.
So I started working on Ordina V1.9.
The Year Before
This work did not begin in October.
Ordina had already grown beyond its original form. Version 1.6, around July this year, reworked our delivery engine. Version 1.7 finalized the loyalty-points system. Version 1.8 introduced pickup orders, alongside the broader idea that an order doesn't necessarily have to be delivered at all. Ordina was no longer something I could accurately describe as just a food-delivery app.
The subtler work happened underneath. Early in 2026, Ordina expanded into groceries and market products, which forced us to reconsider assumptions we had made when the platform only served restaurants. It took a couple of months of refactoring before the catalog architecture reflected how the business actually operated — and much of that work was already in motion long before the deadline (Oct 15) existed. The two weeks were the sprint that forced all of it to converge into something merchants could use.
Chapter 1 — The Problem Was Never Delivery
One side of the business had not kept pace with the platform's evolution: the tools available to the businesses operating on Ordina.
We needed to give business owners more control over their operations, better access to their numbers and reports, and the ability to update prices and add or edit products without depending on our team for every change. Every routine update that required our intervention added friction, and as we expanded into new sectors that friction would harden. Onboarding more kinds of merchants couldn't depend on us manually managing every detail forever — and competitors weren't offering this kind of workspace, which made it an opportunity worth taking seriously.
I didn't need another internal dashboard that happened to expose a few database fields. I needed a proper workspace through which a business could manage its presence on Ordina.
That became Ordina Business.
Starting with the system we already had
I wasn't starting from zero. Ordina already had a substantial backend, an established item model, catalog infrastructure, authentication, and the logic powering the customer-facing applications. I didn't want to duplicate any of that just to build a merchant interface.
A product-management interface might look like a collection of forms, tables, and buttons. But every action in that interface has to fit into the underlying system. When a merchant changes a price, which record is authoritative? When they add a product, is it a new global catalog item or a listing belonging to their shop? When a business has access to several shops, how do we know which shop the current request may modify?
Those aren't details that can be postponed until the interface is finished. They determine what the interface can safely do in the first place. I wasn't simply building screens on top of an API, I was testing the shape of the platform itself.
Two weeks, one developer, and a moving target
I worked on Ordina Business largely as a solo effort. That let me decide quickly, but it also meant the same person owned the architecture, backend changes, frontend implementation, debugging, testing, and deployment. A feature that looked small from the merchant's perspective could require changes across the data model, authorization layer, API, and frontend.
AI-assisted development became one of the tools I used along the way, helping me explore approaches and simplify designs that had accumulated too much complexity. But the responsibility for deciding what belonged in the product, how it should behave, and whether the result was sound remained mine.
The deadline imposed a useful constraint: I had to separate what the product needed to be from what it needed to contain by October 15. I wanted something that could support merchants long term, but also a version that could be deployed and tested with real usage — and that had to come first, without compromising the foundations.
By the time I began writing this journal, the application was being tested with two or three merchants. That was enough to expose the difference between an application that works in development and one that someone's business depends on.
The two-week sprint, and the long tail of platform work it sat on top of.
The next question was not how to make a menu screen or add another API endpoint. It was how to give merchants control over a platform whose catalog and item architecture had itself evolved over months.
Chapter 2 — When a Food App Became Something Bigger
Building a delivery application for restaurants feels straightforward. A restaurant has a menu, the menu contains items, and customers browse those items before ordering.
Then you start expanding. A supermarket might sell thousands of products, many of which other supermarkets also sell. The same packaged product can appear in several shops, with different prices and availability, while still representing the same underlying product. Markets stretch the catalog further still.
The question is no longer how to store a restaurant's menu. It's how to represent products across an entire platform while letting individual businesses decide what they sell.
One platform, different kinds of businesses
The goal of the multi-vertical transition was one platform capable of serving different sectors, each with its own behavior and configuration — not three separate applications with three unrelated data models. We introduced the Service Module model to configure service types and their settings, so Food, Groceries, and Market could be distinct service types within the same broader system. But that was only part of the problem: the catalog also needed to understand which products belonged where, who could use them, and how they related to individual shops.
Our first attempt reused the product-grouping structure the platform already had, treating certain groups as catalogs instead of introducing a separate concept. It was a practical way to move quickly without rebuilding everything, but it quietly overloaded those groups with responsibilities they were never designed for. It was clear as the platform expanded, the meaning of a catalog began to drift.
The shared catalog made sense — until it didn't
A shared catalog is a natural fit for groceries. Imagine two supermarkets selling the same bottle of cooking oil. We don't want two unrelated product definitions just because two shops sell it; a shared catalog lets both shops reference the same product while keeping their own listings and prices.
Restaurants are different. Two independent restaurants might both sell a chicken burger, but those burgers can have entirely different ingredients, descriptions, and identities. Treating their menus as one shared catalog creates more problems than it solves. Franchises are the important exception: multiple branches of the same restaurant may genuinely need to share a catalog.
So we had three requirements at once:
- Grocery and market shops needed to reuse products from a shared catalog.
- Independent food businesses generally needed catalogs of their own.
- Franchises needed the option to share a catalog across multiple shops.
The original approach couldn't represent all three cleanly, because a group describes where an item sits in a hierarchy; not who owns the catalog, which sector it serves, or who may change it.
The difference between a product and a shop's listing
This is the spine of the rest of this journal, so it's worth being precise about it.
A product in a shared catalog is not the same thing as the listing a customer sees in a particular shop. The catalog describes the underlying product; the shop listing represents how that shop sells it.
Figure 1 — One catalog product, two shop listings. The catalog product is shared; price and availability belong to each shop's listing.
This separation became even more important once product variants entered the picture, but it was already necessary: without it we would either duplicate product information or struggle to represent how shops differed.
The architecture was starting to fight the product
The multi-vertical migration was not a failed design. It was an intermediate design serving a platform that was changing faster than its original assumptions — a group tree, catalog markers, public groups, and migration utilities whose relationships were becoming harder to reason about.
Consider the questions the system needed to answer:
- Which catalog does this product belong to, and which shops can access it?
- Can a shop modify a catalog or only use its products — and if it can access a large catalog, must it be allowed to use every group within it?
- How do we preserve the shop-specific price and availability of an item?
Those aren't all the same problem, and solving them through one group hierarchy would only make the system more complicated. It took a couple of months to refactor the catalog architecture into something that reflected how the business actually operated. The kind of work that turned out to be the groundwork for the merchant application.
The next step was to stop treating the catalog as a property inferred from a group tree and give it an explicit place in the data model.
Chapter 3 — The Catalog Was Never Just a Catalog
A group can tell you where an item belongs in a hierarchy. It cannot tell you who owns the catalog containing that group, which shops may use it, or whether those shops can modify the underlying catalog. Those are different relationships, and they needed to be represented as different relationships in the data model.
Giving the catalog an identity of its own
The architectural change was to introduce a first-class Ordina Catalog model. Instead of inferring the catalog from a collection of groups, we represented it explicitly, with its own identity: which service type it belonged to, whether it was enabled or not.
That put the responsibility in the right place. The catalog could now say which sector it belonged to, and be switched on or off on its own.
The other half of the change was Catalog Access. A catalog existing and a shop being allowed to use it are two different things. Access records express that separately: a shop can be granted read-only or read-and-write permission on a catalog, which let us represent very different operating models. One shop might choose products from a shared catalog without being able to alter the shared definitions at all. Either way, the shop's own listings still carry the prices and availability that the shop controls.
Figure 2 — Catalog ownership and access. Access grants are illustrative; permissions are enforced by the backend, and catalog access does not imply permission to perform every operation.
The important part wasn't how many models we added, but that each had a clearer responsibility: Ordina Catalog answered Which catalog is this?, Catalog Access answered Which catalogs can this shop access, and with what permission?, and the item models answered different questions about the product and how it was sold.
One architecture, different defaults
A single policy would not have reflected the way our sectors operated. Food shops would receive their own catalogs by default, because independent restaurants generally have their own menus; sharing could still make sense for franchises. Groceries and Market would use shared default catalogs by service type, so multiple shops could reuse catalog products instead of creating independent copies of every product they sold.
A default is a policy for assigning catalogs, not a rule that every shop must have an entirely separate product universe.
The difficult part was moving the existing data
Designing the new schema was the easy part. The existing data was the hard part.
The old system already held catalog items, shops, and group structures — all of them created before the new architecture existed. The migration had to place every one of them into the correct catalog without damaging the relationships they already had. Most could be placed confidently; some could not, and those had to be flagged for a person to check rather than quietly guessed at.
That distinction matters more than it sounds. During a migration, giving every row an answer is not the same as giving every row the right answer. For food items, the correct catalog was normally the one belonging to the shop that sold the item. But when an item's shop reference was missing, or pointed at more than one shop, there was no reliable way to know which catalog was meant — so the migration refused to guess and left those rows for manual review.
Figure 3 — Migration assignment policy. Ambiguous rows are surfaced rather than silently assigned. The patch also supported a dry-run mode.
This is the kind of work that never shows up in a feature announcement — there is no impressive screenshot of a migration patch. But it decides whether everything built afterward sits on data you can trust.
Chapter 4 — A Shared Catalog Is Still Too Big
Imagine a grocery supplier with thousands of products organized into categories. Several shops can access the same catalog, but each may sell only a portion of it. One shop might specialize in household goods and packaged food; another might carry a broader grocery selection. Giving both access to the entire catalog is technically convenient, but it doesn't necessarily reflect how they operate.
That led to Shop Catalog Scope. A shop could work with a selected portion of a catalog's group hierarchy, including the descendants of any selected parent group; We weren't creating a catalog for every shop or copying products into separate trees — we were defining which part of an existing catalog a particular shop could work with.
Access and scope answer different questions
Catalog access answers which catalog can this shop access, and with what permissions? Catalog scope answers which portion of that catalog is this shop allowed to use? Neither replaces the other.
Figure 4 — Access versus scope. An empty scope means unrestricted use of the accessible catalog.
Scope also had to preserve the integrity of the hierarchy. Group selections couldn't be an arbitrary list. The implementation had to prevent redundant parent-and-descendant selections. It affects which catalog products a shop can legitimately work with.
Then came the problem of two group hierarchies
We had a catalog hierarchy, but shops already had their own group structures. A shared catalog needs a way to organize its global products; each shop needs a way to organize its own menu or inventory. The two structures can be related, but they are not interchangeable — a catalog might have a group called Beverages while a shop has Drinks. The challenge wasn't matching names, since names and structures differ and a shop's organization can evolve independently of the shared catalog. We introduced mapping records to express those relationships explicitly.
Mappings became the bridge
We already had a mapping that ran from a catalog group to the shop's own group, and older workflows depended on it. Ordina Business needed the reverse as well: given a shop group, work out which catalog group sat behind it. So both directions existed, and keeping them in agreement became a small source of complexity — the idea was the same, but where you started from decided which record you followed.
Figure 5 — Two hierarchies connected by explicit mappings. Group names are illustrative.
The mapping layer also had to support existing behavior: the older dashboard and item-creation flows couldn't stop working because we had introduced a new direction of mapping. Where appropriate, the reverse mapping could inherit from the nearest mapped ancestor, and we added best-effort synchronization into the forward mapping so existing workflows kept the relationships they needed.
Chapter 5 — Putting a Business Around the Existing System
Who, exactly, was using Ordina Business?
Ordina already had authentication and established ways for customers and shops to interact with the platform. But a merchant workspace introduces a different relationship between users and the system: a person might belong to a business operating several shops and be allowed to manage one but not another, and someone coordinating the business might need broader access than an employee who only updates products. I didn't want to treat every authenticated user as an administrator, nor build a separate user-management system disconnected from the platform — we needed a layer on top of what we already had.
A user is not a business, and a business is not a shop
A user is the person authenticated by the system; a business is the organization they work with; a shop is an operating entity within that business. Those relationships shouldn't be collapsed into a single user record or inferred from whichever shop happens to be open. The backend introduced business membership and shop-access records to represent them, establishing not just who had signed in, but which business they belonged to and which shops they could access.
Figure 6 — Identity and access in Ordina Business. Actual permissions depend on membership, shop access, role, and the operation requested.
This gave us a foundation for multi-shop businesses, and it let the application switch between shops while retaining the user's business identity.
Roles need to express actual responsibilities
We introduced role-to-permission mappings rather than scattering role-name checks throughout the application. The business side had Member, Admin, and Owner; the shop side had Staff, Operator, Manager, and Supervisor — business-level authority and shop-level operational access aren't interchangeable. The important principle was that permissions belonged in the backend: the frontend could use role information to decide which actions to display, but hiding a button was not sufficient authorization, and a request that changes a product still has to be validated by the server.
The headers select a context; they don't grant permission
Ordina Business used the X-Ordina-Business and X-Ordina-Shop headers to identify the selected business and shop context, which made the frontend straightforward: when a user switched shops, the API client sent the corresponding context with subsequent requests.
But these headers were not credentials. A client can send arbitrary header values, and if the backend treated the presence of a shop ID as proof that the user had access to that shop, the entire access model would be undermined. Instead, the backend resolved that context against the relevant records and authorization rules — establishing the authenticated user, verifying their relationship with the business, and checking access to the requested shop. The header answered Which shop are you asking to work with? It did not answer Are you allowed to work with it?
Figure 7 — Resolving a request. The headers select a context; the records and rules decide whether the operation is allowed.
Context resolution and authorization decorators
I wanted this logic reusable rather than reimplemented in every endpoint. Existing decorators such as @ordinacore.customer_whitelist already provided patterns for restricting particular kinds of requests; the business application needed corresponding primitives. An endpoint can now declare the access requirements that apply to it while a shared layer handles the common checks, which reduces the risk of one endpoint quietly implementing a weaker version of the same check — and lets us distinguish the authorization mechanism from the business logic.
The frontend followed the same model, with an application-level context store and a shared API client that attached the headers, plus an application shell handling authentication bootstrap, route guards, and unauthorized states. The shop switcher became more than navigation: changing the selected shop changed the context for subsequent operations.
Reusing the platform instead of rebuilding it
A deliberate part of this work was keeping Ordina Business connected to the existing platform. Building a new frontend didn't mean inventing a second authentication system or duplicating the product data; the business layer was introduced alongside the platform, with its own membership and access concepts, using the established backend infrastructure.
The foundation was in place. Now we needed to build the part of Ordina Business that merchants would actually spend their time using.
Chapter 6 — The Menu Is the Application
There is a point in building a merchant application where the backend stops being the most visible problem. You can have a sensible data model, authorization rules, and a catalog that correctly represents products across multiple shops — but if a merchant struggles to find an item or change a price, none of that matters much to them.
For a merchant, the application is the menu: the place where they do the actual work of running their business on Ordina.
First, we had to make groups make sense
The group architecture had accumulated several layers: catalog groups for shared products, shop item groups for a shop's inventory, mappings between them, and catalog scope limiting which parts of a catalog a shop could use. All legitimate — the problem was that the merchant shouldn't have to understand any of them to add a product.
A merchant thinks in categories: drinks, snacks, household products. They want to open a category, see its items, move one when necessary, and continue working — so the application had to translate that into operations against the underlying catalog and shop structures.
Group management therefore became more than a CRUD interface. We needed a tree that could be browsed and maintained while respecting the relationship between the shop's organization and the catalog it used, with item counts so a merchant could read a category without opening every descendant.
The mapping problem didn't disappear when we built the UI
A merchant might create or move a shop group without thinking about its relationship to the catalog. But when they add an item from the catalog, the application needs to resolve the relevant catalog group — and that relationship can't always be inferred from matching names. It has to follow the explicit mappings and the rules for inheritance and scope.
This meant group management and item creation couldn't be treated as unrelated features. Changes to the group tree could affect how products were found and how new listings were created.
Figure 8 — Resolving a catalog group from a shop group, with inheritance from the nearest mapped ancestor.
Item creation is where all of it converges
When a merchant adds a product, the application has to decide whether they are picking from an existing catalog or creating a listing that belongs only to their shop. It has to carry the chosen shop context through the request, respect their catalog access and the shop's scope, and create the shop listing with its own price and availability rather than mutating the shared catalog definition.
Variants add another layer: a catalog item can have variants, a shop listing can too, and the relationship has to survive an edit. Editing is harder than creating, because the form already holds data loaded from the server and has to reconcile it with what the merchant is changing. The editor also supports bulk operations and availability controls, since merchants rarely change one product at a time.
None of this is visible in a screenshot. What is visible is whether the merchant can find the category, pick the right product, set a price, and save without a mistake — and that depends on the catalog, access, scope, mapping, and authorization layers all agreeing with each other.
The menu is where architecture becomes a product
Once the menu worked, Ordina Business stopped reading like an internal tool. The architecture was no longer a set of models to reason about; it was what let a merchant pick a shared grocery product, set their own price, and mark it unavailable, without seeing the machinery underneath.
That's the test I kept coming back to: a correct data model is not the goal — a merchant being able to do their work is. But an application is more than its menu, and the details around it determine whether it holds up once real people use it on real phones.
Chapter 7 — Making It Hold Up On A Phone
Product images looked simple until they had to work on a real device, over a real network, with files that came from real users.
A merchant opens an item, takes a picture, and saves it. From their perspective, that's the whole operation. From the application's perspective, the picture has to be decoded, resized, compressed, uploaded, associated with the correct record, stored durably, and reflected in the interface. If any part of that chain fails, the merchant may see a broken image, wait unnecessarily, or find that the change they thought they had saved has disappeared.
One image in, three resolutions out
A phone photo is far larger than a product listing needs, but shrinking everything to a thumbnail is wrong too: the same image might appear in a small list or in a product-detail view that benefits from higher resolution. So the pipeline generated three versions on the client — a thumbnail, a default, and a high-resolution copy — from a single decode of the source rather than three separate files.
Figure 9 — One source image, three resolutions, generated from a single decode.
We preferred WebP with a JPEG fallback and kept the encoding settings configurable from the backend rather than hard-coded in the UI. None of it is exotic; the difficulty was never the format. It was deciding when the work should happen.
Staging uploads behind a queue and a tray
Uploading the moment a file was selected would have been the obvious implementation, but it creates work that often isn't needed: a merchant might choose an image, reconsider, and replace it, or leave the form without saving.
So nothing uploaded on selection. Images were staged in the form instead, and a floating action button surfaced them as an upload tray — one place showing every pending image and its state. The tray gave the merchant visibility and control: they could watch progress, retry a failure, cancel, or drop an item entirely before it was ever sent.
Only on save did the queue start pushing files, one at a time. Serial uploads are slower when there are many files, but a single in-flight request kept the queue predictable and stopped several large photos from competing for the same mobile connection — and it made the tray's progress honest, since each item had one clear state at a time.
The image that disappeared after saving
The hardest image bug I hit was replacing the image of an existing item.
Creating a new item and assigning an image is one workflow; replacing the image of an item that already exists is another, because the form already holds data loaded from the server and saving may trigger a refresh of that data. The catalog-item edit form had a watcher that could reset image state when the item was fetched again — and after the update mutation completed, the frontend invalidated the item data and awaited the refetch.
That created a race.
Figure 10 — The race that made the image disappear. A refetch after the update reset the form's staged image state.
From the user's perspective the sequence was simple: open an item, select a replacement image, save, and discover the change hadn't persisted. The difficulty was that each part looked reasonable — refetching after an update is normal, and resetting a form when its underlying record changes can be reasonable — yet neither explains the bug in isolation. It came out of their interaction.
The fix was to preserve the staged image before awaiting the mutation and its refetch, and to make the form more deliberate about when refreshed data could reset that state. What made it hard wasn't the image format; it was asynchronous operations changing the state of the same form at different times. A save doesn't happen in a vacuum, and a successful API response doesn't guarantee the UI will keep the user's intended changes.
Chapter 8 — The Things That Make It Production-Ready
A menu can load correctly in development. An image can upload on a fast connection. A merchant can sign in, select a shop, and update an item without any obvious problems.
But what happens when the application is installed on a phone? When a new version is deployed while someone still has the previous one open? When a session expires, an API request takes longer than expected, or the browser decides to use a cached resource? These aren't the features that usually make an application interesting in a demo. They determine whether it stays usable after the demo is over.
Making it installable
Ordina Business was built as a progressive web app, which let us deliver a mobile-oriented application through the web without maintaining a separate native app just to provide an icon and an app-like launch. Its manifest described who it was and how it should launch — icons, standalone display, portrait orientation.
A service worker is not a magic offline switch
The most important PWA decision was what to cache. We cached the application shell, separating the static resources that load the interface from the live data supplied by the backend.
That distinction matters because Ordina Business is not a static website. A merchant's menu, prices, availability, and business context change, and the application has to retrieve current information rather than trust a cached API response. So we excluded API endpoints, files, private resources, and realtime connections from ordinary navigation fallback and served them network-only, keeping the cache for the shell.
Figure 11 — What the PWA caches. The application shell is cacheable; live merchant data is network-only.
This was a deliberate trade-off: we wanted an installable application with a managed static shell, not an offline-first merchant system with local copies of inventory and prices. Full offline support is a different project — persistence, stale values, queued writes, synchronization, and conflict resolution — none of which should be implied just because an application has a service worker.
Updates, and deployment
Caching the shell creates a new responsibility: updating it. Ordina Business used a prompt-based update flow, making a new version available and letting the user respond rather than silently interrupting them — a user halfway through editing an item shouldn't have the workflow disrupted when a new frontend version becomes available.
Deployment followed the same principle of reuse: the built static application and the backend were served through a consistent origin, keeping the cookie-based session model simple instead of introducing cross-origin complexity. A frontend that works through a development server is not necessarily ready to be served as a production application, so that was a boundary worth crossing deliberately.
Testing the pieces that were easiest to break
I also built out tests across the backend and frontend, covering business functionality, group operations, item-management workflows, group utilities, menu browsing, the application shell, and PWA installation. The value of a test suite isn't the number of assertions it contains, but whether it catches regressions in the behavior the application depends on — a change to group mappings can affect how products are created, a change to the shell can affect authentication, and a change to installation logic can resurrect prompts a user already dismissed. Testing doesn't replace testing on a real device, but it lets us check important behavior repeatedly as the implementation evolves.
Chapter 9 — Learning to Trust a Migration
Getting Ordina Business into production meant changing a live system. The backend alone added over 17,500 lines of code for new authorization models, catalog structures, and item workflows. That change had to be applied to a platform that wasn't empty — it already had users, data, and established behavior that other parts of the system depended on.
A migration is a statement about your assumptions
A migration script is a statement about your assumptions. It expects data to look a certain way, but a live database rarely does. The catalog migration was a good example: we were moving from an inferred structure to an explicit one, and every existing item had to be assigned to the correct new catalog. The script could handle the clean cases, but where an item's history was ambiguous, it couldn't safely guess. A dry run helps you see this, but it doesn't fix it. The data itself shows you where your assumptions were too optimistic.
The part that doesn't fit into a script
That's the part that doesn't fit into a script. I spent hours on those cases, late into the night, fixing records by hand. You can design a clean architecture, but production data is a record of the system's history, not the model you wish it had. When a script can't resolve a case safely, the only responsible choice is to stop and handle it deliberately. There's no elegant diagram for that work, but it's what makes a structural change trustworthy.
Running a migration is its own problem
Even how you run the script matters. For long operations, I switched from nohup to tmux to get a persistent session I could detach from and reconnect to. The tool doesn't make the migration safer, but it gives you a stable window into a long-running process, which mattered after a previous nerve-racking experience.
The deployment is bigger than the diff
The 17,500-line diff doesn't tell the whole story. The hard work was spread across layers: new models had to work with old records, new APIs had to avoid disturbing existing clients, and new workflows depended on permissions being right. That's why deployment isn't a mechanical step after development — it's part of the engineering. A feature can be correct in isolation and still cause problems in a live system. The goal isn't just to make new code run; it's to introduce new behavior without losing control of what already exists.
What I took away from it
That's what I mean by learning to trust a migration. It's not about trusting a script because it ran once. It's about understanding the gap between the model you're introducing and the history baked into the data you already have. Trust comes from knowing the assumptions, inspecting the effects, and not ignoring the awkward cases.
Chapter 10 — The Final QA Sweep
As October 15 approached, the nature of the work changed. For most of the sprint the challenge had been building; near the deadline it became making sure everything continued to work together.
Testing wasn't confined to the merchant application. The frontend depended on shared backend functionality, and changes there could affect existing workflows elsewhere in Ordina. Some fixes were quality-of-life improvements; others addressed regressions that needed correcting before we could ship. They weren't all bugs introduced by Ordina Business, and I don't want to rewrite the history as if every change was a consequence of this project — but they all mattered to the environment the new application had to operate in.
That's a less glamorous reality of building on an established platform: you don't get to work only on the code you intended to change. Your application inherits the behavior, assumptions, and occasional problems of the systems underneath it. The hard part wasn't fixing each issue, but understanding its scope — whether it belonged to the new application, the shared platform, or the interaction between the two — while still separating essential fixes from improvements that could wait.
Deployment is where assumptions meet reality
Getting a feature to work locally is not the same as deploying it into an environment where the frontend, backend, database, background jobs, and existing clients all have to cooperate. The final stretch reinforced that: fixes had to be tested in context, and changes that appeared small in isolation could have wider effects.
It wasn't a perfectly smooth process, but it also wasn't a single catastrophic failure or a dramatic rescue. It was the ordinary, sometimes frustrating work of getting a fast-moving project into a state where other people could depend on it.
Chapter 11 — It Actually Felt Native
There was one thing I had been quietly worried about throughout the project: would the application actually feel good to use on a phone?
I knew I could build the functionality. What I wasn't sure about was how the whole thing would feel when installed and used as an application rather than opened as a website in a browser. A PWA can be installable without feeling particularly native — an icon on the home screen and a standalone window, yet still sluggish compared with a proper mobile application.
I didn't want Ordina Business to feel like that.
During development it's easy to get used to the mechanics — run the development server, open the browser, think about components, requests, and state — and lose sight of what the finished product will feel like to someone who doesn't care about any of that. Installing it on Android changed that perspective: I opened it as an installed application and it felt snappy and sleek, much closer to what I wanted than the sluggish experience I had been bracing for.
That moment mattered to me. I had spent days working through details a merchant would never see, and then installed the application and experienced the result as a user.
It felt like an application.
Enjoying the installed application didn't mean the work was finished. The application still needed testing with merchants, whose real usage would reveal problems I couldn't discover myself. But the experience gave me confidence in the direction we had taken: we hadn't needed to abandon the web stack to create something that felt at home on a phone.
Chapter 12 — October 15 Is Not the Finish Line
October 15 was a deadline driven by the business, not a date on a carefully planned roadmap. I had an idea of what needed to exist, a platform I could build on, and roughly two weeks to turn the idea into something merchants could actually use.
As that deadline approaches, Ordina Business is no longer just an idea in my head or a collection of components in a repository. It has its own business and authorization model, a catalog-aware item-management workflow, a mobile-first interface, image processing and uploads, and an installable PWA experience. More importantly, two or three merchants are testing it.
That's a meaningful change from where we started. It doesn't mean the product is finished.
The difference between building a feature and solving the problem
It's easy during a fast sprint to measure progress by the number of features completed. We added catalog search, item creation, variant management, bulk operations, availability controls, image uploads, group management, and authentication flows — but the reason for building them was never to accumulate a long feature list.
The objective was to give business owners more control over their operations on Ordina. Could they manage their products without depending on us for every change? Could they update prices and product details? A technically correct item editor is useful only if merchants can use it comfortably, and a reporting endpoint is not the same as a report that answers a business owner's actual questions.
The first feedback is already changing the picture
One of the early areas merchants wanted was better support for discounts and offers. Another was product descriptions and comments.
The second is particularly interesting, because it highlights something easy to underestimate when designing a product catalog. It's tempting to assume that a good product image does most of the work, but for the market segment we serve, pictures alone aren't always enough — written descriptions and details help customers understand what they're ordering. The best workflow isn't necessarily the one with the most elegant data model; it's the one that gives merchants the tools they need.
Discounts deserve a closer look too. Item-bound discounts had effectively been a no-op since the multi-vertical migration and the introduction of variants — an existing limitation that had been deferred, not something discovered during testing. A merchant-facing application makes such gaps harder to ignore: managing an item's price is useful, but merchants also need a coherent way to manage promotions and offers.
Reports were still on the list
Reporting was another part of the original goal, and it was still in progress as the deadline approached. Giving merchants control over their products was only half of it — business owners also needed better access to their numbers, so they could understand how their businesses were performing without depending on our team to pull the figures.
The progress elsewhere couldn't hide that. Item workflows were taking shape, the PWA was becoming usable, and merchants were starting to test the product, but a key part of the value was still being built.
I wanted the reports finished before October 15, ideally earlier — while the application still needed testing and merchant feedback was already pointing at other improvements, including discounts, offers, and richer product descriptions.
That's the reality of a business-driven deadline: the product can be ready to put in front of merchants without every important capability being done. The remaining work wasn't an afterthought — it was part of the product we had set out to build.
The work continues
Looking back, the most significant result wasn't any individual feature. It was the transition from a platform that could technically support merchant operations to an application through which merchants could perform them — which required the catalog, authorization, mapping, and frontend layers to all agree, supported by the deployment and tests around them.
None of those pieces alone would have produced Ordina Business; they became valuable because they worked together. There were compromises, frustrating bugs, and decisions made under time pressure. AI-assisted development helped me move through that process, but the project still required engineering judgment: deciding what the product should do, which assumptions needed to change, and which problems were worth solving before the deadline.
The immediate priorities were clear: finish the urgent reporting work, address the gaps that mattered most to merchants, keep testing, and let feedback guide what came next. The deadline gave me a reason to build the first version quickly. The merchants would help determine what it needed to become — the part no architecture diagram or passing test suite can answer on its own.