norveon.
Retail distribution

Inovio Hub

Inovio Hub interface
Client
Inovio doo
Year
2026
Duration
9 months, ongoing
Industry
Retail distribution

The challenge

Inovio doo buys from distributors and sells through marketplaces, partners and its own storefront. Ordinary ecommerce software assumes a product has one supplier and one price. Here it has several of each, and every channel has its own commercial terms. The old catalog could not hold that, so people reconciled the difference by hand. The work grew with suppliers multiplied by channels, which put a ceiling on how much the business could take on.

Our approach

We built a platform where the catalog is the single source of truth and every external system sits behind an adapter. Supplier feeds land in one internal shape whatever format they arrive in. Every sales channel is fed from that same shape. One pricing engine produces the number that gets published, so nothing recalculates on the way out, and stock is worked out in one place. An AI enrichment service fills the fields the feeds do not supply, and a person approves its proposals before they count. The back office is a dense operational tool for people who scan long tables all day, not a marketing surface.

Results

In production and shipping continuously since late 2025. The migration kept the product codes that sales channels already had on file, so nothing had to be relisted. Adding a supplier or a channel is now contained work instead of a change that ripples through pricing, stock and every existing integration. Data entry that used to sit in a queue now arrives as proposals an operator approves.

  • 4,703 automated tests across 461 test files
  • 106,000 lines of test code against 109,000 lines of application code
  • 164 API endpoints and 199 back office components
  • 288 tickets delivered across 559 commits
  • 23 architecture decision records and 90 pages of documentation
  • 9 months of continuous delivery, still shipping

What took the time

The hard parts were not the ones anyone estimated.

Pricing was the first. Once a product can be bought from several distributors and sold through several channels, the price stops being a field and becomes a calculation. The system we took over made it worse by recalculating prices inside the code that built each feed, so the number in the database and the number on the marketplace could disagree and nobody could say which was right. We moved all of it into one engine. When we removed the last of those export-time rewrites, some published prices moved. That was an uncomfortable conversation, and the right thing to do.

Identifiers were the second, and we underestimated them. Every sales channel already knew the catalog by the codes the old system issued, and those codes sit in other companies’ databases attached to listing history. Clean new ones would have meant relisting everything from scratch. So a migrated product keeps answering to its old code wherever a channel already knows it, and carries a new internal one everywhere else.

When it broke at four in the morning

A scheduled feed job started running the worker out of memory. Every retry peaked near three gigabytes.

The cause was ordinary, and entirely ours. The export built the whole feed as one string in memory, compression held the original alive, and an error later in the upload kept any of it from being freed while the queue retried.

Nothing customer-facing went down, which is the point of separating background work. But the feed did not go out, and it would have failed again the next night. We rewrote the path to stream: a database cursor feeding an XML writer, compression and upload both streaming instead of buffering. Then we capped container memory, cut the retries for that job type, and wrote up the cause and timeline.

We would rather show a prospective client this than a clean status page. The question worth asking whoever builds your system is how fast they find out when something breaks, and what exists afterwards besides a fix.

How it runs

Deploys go out from CI as containers with memory caps, so one bad workload cannot take the host down. Background work runs on a Redis-backed queue with a dashboard. Logs are structured and searchable, errors go to Sentry, and inbound supplier feeds sit behind a circuit breaker so a distributor having a bad morning gets backed off rather than retried into the ground.

The part operators use most is the run log. Every sync and feed export records its stats, duration, trigger and outcome. Warnings are counted separately from errors on purpose, so a run that completed but did something worth knowing about reads as clean. Get that wrong and the dashboard sits permanently amber, and people stop looking at it.

What you get besides the software

Twenty-three written architecture decisions, ninety pages of documentation, and a glossary that fixes what every term in the domain means. It sounds bureaucratic. It is why a new person is useful on this codebase within a week.

And tests, in the proportion above: roughly as much test code as application code. In a system like this a quiet bug in pricing or stock does not show up as an error. It shows up as wrong numbers on somebody else’s marketplace, weeks later.