BLUD: every drop of donated milk has a name
How we moved the software that tracks donated human milk to React and TypeScript, after more than a decade locked to Silverlight and Internet Explorer.

Sometimes a woman who has just given birth produces more milk than she needs. Rather than waste it, she can donate it.
A few days later, in an incubator miles away, that milk becomes the first treatment for a baby born too soon, whose own mother cannot produce it yet. Two women who will never meet, joined by a bottle.
For that baby it is not simply food: it is a therapy.
Behind that bottle there is a donated human milk bank, and behind the bank there is software that governs every step. This article is about how we brought that software up to date.
For a premature baby, donated milk is treatment
Let’s start with why it matters: for a premature baby, human milk is not a preference, it is the best available treatment.
Massimo Agosti, president of the Italian Society of Neonatology, has called it “a real, life-saving medicine”: a “therapeutic nutritional bridge” until the mother can produce her own milk (in Italian: ANSA, 16 May 2025).
On the most studied effect the evidence is solid. The most recent Cochrane review (Quigley et al., 2024) pooled 11 trials and 2,261 very preterm or very low birth weight infants: feeding them donated human milk rather than formula halves the risk of necrotizing enterocolitis (RR 0.53), one of the leading causes of death.
The problem is supply. According to the fourth survey by the Italian Association of Donated Human Milk Banks (AIBLUD), carried out in February 2025 and covering 2023 and 2024, Italy leads Europe with 44 active milk banks, ahead of Germany (37) and France (36), with 1,523 donors and close to 10,000 liters collected.
Yet only one fragile newborn in three receives it, because the banks are unevenly distributed and poorly connected: just 32% are linked to any other. Donated milk is precious and scarce, not a drop of it gets wasted, and there is no room for mistakes: the patients are as defenseless as patients get.
Every bottle has a history, and it has to be certified
That is where the problem a piece of software has to solve begins: a milk bank is, in effect, a small pharmaceutical supply chain, and every bottle has a history. Who donated it, when it was collected, at what temperature it was frozen, when and how it was pasteurized, which tests it passed, which hospital it reached, who administered it.
If a single link breaks (a cold chain interrupted, a test that comes back out of spec), that bottle has to be blocked before it reaches a baby.
Full traceability is not paperwork: it is safety.
The software worked, the browser running it did not
There is a part of this story that is invisible from the wards. The system that has supported milk bank work for years is a Silverlight application, the Microsoft technology once used for rich internet applications, meaning desktop software inside a browser: at the time it was a perfectly sensible choice, and it did its job well. It is still in use today, by a lot of people.
Then the ground shifted underneath it. Microsoft ended support for Silverlight, and the plugin only ran on Internet Explorer, which was itself retired (Microsoft Lifecycle).
Just how awkward that has become was clear as soon as we set up a development environment. To open the application we wrote ourselves a PowerShell script that instantiates Internet Explorer through COM automation, because Silverlight runs nowhere else; and among the failures the script anticipates is Internet Explorer having been uninstalled from the operating system, with instructions for switching it back on. On a recent machine that is not a remote possibility.
The software worked. It was the environment it ran in that had reached the end of its life.
What was underneath: more than a decade of VB.NET
Before touching anything we went deep into the existing code. Over 122,000 lines of VB.NET, split between application logic and the plumbing that connects to the services. Dozens of screens and windows, built with DevExpress, a commercial component library of that era. Around a hundred functions exposed by the web services, sitting on several SQL Server databases.
Data access ran to more than 9,000 lines and over a hundred public functions, with business rules and database queries in the same place. Underneath was LINQ to SQL, the layer you talked to a database with back then. That was normal, in a lot of projects from those years. So was the way donors were notified, with text messages handed to a GSM modem: a pragmatic answer to a real problem.
The most telling detail is the dated comments the original developer left in the code as a changelog: the oldest is from January 2012, the most recent from September 2021. They say what changed and why, from a database server migration to a new hospital to serve.
It is worth pausing on that before getting into the technical detail: more than a decade of service in a healthcare setting, with no rewrites. That software worked, and whoever wrote it did a good job with the tools of the time.
Understanding 122,000 lines of VB.NET, with help from AI
Nobody on the team knows VB.NET deeply, and the domain has a vocabulary all of its own: donors, bottles, batches, boxes, trays, pasteurization. The code uses it without explaining it. Before we could design a single new screen we had to reconstruct what the old one did, and the source code was the only documentation available.
This is where the model saved us weeks, not writing code but reading it. Feeding it a 900-line screen and asking which service methods it calls, in what order, and what it does for each return code compresses hours of work into minutes; the map of the ten states in a bottle’s life came out the same way.
Where the model got it wrong, and what we learned
It got things wrong, and not randomly: plausibly. Three statements about the behavior of the same freezing screen, all false:
- “after every scan the input is disabled, the operator has to click Add again” - in fact the input only locks during the server call, then scanning resumes on its own;
- “it asks ‘Print the label?’ with Confirm and Cancel” - in fact a warning appears with a checkbox already ticked and a single OK button;
- “it asks ‘Confirm exclusion?’” - in fact it asks nothing at all: press OK and the bottle is already excluded. The window informs, it does not ask.
The cost: several iterations on the same page, and decisions made on wrong information.
The problem was not the model, it was the method. A name like DisabilitaInsert suggests a meaning the code does not have, and interface behavior does not live in the code that talks to the database: it lives in the file that describes the screen, and in the window that screen opens. That is where you have to read it. The model filled the gaps with the most reasonable inference, which on a legacy system is often the wrong one.
Several rules came out of that and governed the rest of the port. Never state a behavior of the old system as fact without having verified it in the code, citing file and line. When the information is missing, do not fill the gap with a guess: say that it is missing. And if someone is making a decision based on our description, that description gets verified first, not afterwards.
AI, in all this, was an accelerator for reading and not a source: it cut the time it took to reach the right hypothesis, while verification stayed human work.
The constraint: rebuild the frontend and leave the rest alone
The approach was as surgical as possible: rebuild only the frontend, meaning everything the operators work with every day, and leave the backend where it was, with the clinical logic and the data that years of use had proven.
This was not a technical preference but an explicit project constraint: the backend does not change, and where it genuinely has to, you touch the minimum, while everything else gets solved on the client.
The new application is written in React and TypeScript, with type checking at its strictest.
Where it got to, over several months of work: 38 screens across 21 areas of the domain, 55,600 lines of code in 323 files. There are 106 points of contact with the existing backend, 56 reads and 50 writes.
The 1,449 phrases in the interface all live in a single dictionary, outside the code. Changing a word, or adding a language, does not mean touching the program. And forms are not written by hand: they are described as data, with eighteen field types. A new screen gets composed, rather than built from scratch.
For developers: Vite for the build, MUI for the interface, Redux Toolkit and RTK Query for state and requests, CASL for permissions, Plotly for charts.
Every anomaly recorded, with the evidence beside it
That constraint has a consequence you feel every day: when you find a behavior that is wrong you cannot fix it, you can only know it and work around it the same way every time. Hence the part of this work we are proudest of, which is not code but a file.
Every time we met unexpected backend behavior we recorded it in a catalogue, under one strict rule: only what has been verified gets written down, confirmed in the code with a file and line reference or in a real server response. No suspicions and no deductions, and every entry states the cause, the impact on the frontend, and where the workaround lives.
When one of those anomalies was fixed at the root the entry was not deleted: it stays in the file as history, with the evidence of the test that proves it, because the reason behind a decision is worth as much as the decision. It is the kind of work that never shows up in a demo, and that decides whether a system like this will still be maintainable in five years.
Where the constraint gave way
Working around them was not always enough, though. A new interface needs to speak the right language, and the backend had been designed for the SOAP clients of its day, the language you talked to services with then: on four points there was no alternative. Targeted work, then, not a rewrite: enough to carry the change and nothing beyond it.
We enabled the services to answer in JSON as well, which is what a browser understands; that switch surfaced a few configuration limits we had never run into before, all of them resolved. On top we added a token authentication module, without touching the logic of the services. Then fourteen database indexes, on the columns the lists and searches use, with the script to remove them alongside: they were not there, and with the data volumes of the time they probably were not needed.
The most visible point is speed, and these are the two numbers we are happiest with. The permissions matrix used to take 1,442 milliseconds, because the backend offers two ways of getting the same data and the more expensive one was being used: we switched it and rebuilt the matrix on the client, and we are at 31. Event search used to take 3.4 seconds because it ran a fresh query for every row of the result; removing that redundant round trip brought it down to 0.07. This is not polish: it is the difference that let us replace a “Show” button with a search that updates as you type.
We also added a few things that were missing: restoring several blocked bottles in one operation, reading stock filtered by date, and making the registration of new batches transactional, so an interruption halfway through does not leave partial state.
The work spread as far as it had to, but it stayed under control. The core of the system is still the one that was there before.
Ten steps, ten signatures
For the people who use it, the day changes: where a desktop workstation with an unsupported browser used to be necessary, now an up-to-date browser is enough and there is nothing to install.
Permissions follow the real roles, whoever manages the donors, whoever runs pasteurization, whoever handles the hospital store, and each of them sees only what concerns them. Step by step the application follows every bottle along its whole journey: collection from the donor, freezing, thawing, pasteurization, a second freezing, boxing, pickup, delivery, arrival in the hospital store, and withdrawal for administration to the baby.
Ten steps, and each one leaves a trace in the bottle’s history: what happened, when, and the operator who recorded it.
An interface for the ward, not for a showcase
A deep blue as the anchor color, red for critical states, and a number of customized components to get high information density without noise. Whoever opens the application lands on their own dashboard, and permissions decide which one.
A lot of the choices come from the people working with a scanner in their hand. After every read the input takes focus again on its own, because bottles are scanned in sequence and hands are full: reaching for the mouse between one code and the next slows everything down. The audible signal changes with the severity of the outcome, at a volume tuned for the actual workstations, where the background noise is not an office’s. Blocking errors stay modal windows rather than notifications that vanish by themselves: if a warning goes unnoticed, a bottle moves on when it should not, and that is not a usability problem but a safety one.
Scanning windows can be dragged, because sometimes they need to sit next to something else. Grids export to Excel, reports to PDF.
If the process is out of spec, the batch is blocked
Milk is a therapy, and the system behaves accordingly.
Pasteurization follows the Holder method, 62.5 °C for 30 minutes, the treatment recommended by international milk bank guidelines, including those of the European Milk Bank Association.
But it is not the software that decides whether a cycle is within spec. The system collects the temperature curve and displays it against the process thresholds, then asks an operator to certify the outcome: in spec or out of spec. The certification is stored with a name, a date and notes.
Everything that follows that signature is automatic. The outcome propagates to every bottle in the batch, and if the cycle is out of spec the bottles are blocked: from that moment the system refuses every further step and every withdrawal.
On timing, though, the software decides. At every change of phase the backend recalculates the distances between dates: how long from donation to collection, how many hours thawing lasted, how many days the bottle stayed frozen.
If a threshold is exceeded the system refuses the step and the bottle leaves the program. What the operator gets is a warning to acknowledge, not a choice to make.
And on the cold chain the operator is not involved at all: when a freezer raises a serious alarm the refrigeration system notifies the software, which blocks every bottle inside that machine. Unblocking them, where the case allows it, has to be done by a person.
Every bottle carries a unique code, printed on a label and readable by a scanner, which already encodes the essential references of the donation: in the lab a glance and a beep should be enough to know exactly what you are holding.
Where we are now, and who deserves the credit
The old system is still the one carrying the daily work, for a lot of people. The new application is in testing and will be adopted shortly: the switch will happen when the people in the ward have finished trying it in the field.
What we take away from these months is a conviction: on software with more than a decade behind it and real users, the hard part is not writing the new code, it is understanding the old code completely before replacing it, and resisting the temptation to rewrite what works.
The credit for all of this, to be clear about it, belongs to the women who donate that milk, to the people who collect and process it, to the neonatologists and nurses who carry it to the incubators every day. The care is theirs. We contributed one piece, the digital one: the tool that holds the whole chain together and makes sure no step gets lost.
Because traceability, in the end, is a form of care. It means every drop of that milk has a name, and is safe for the fragile baby who receives it.
We like working on projects that are useful to someone, and this one really is.
Do you have a system that works but runs on technology at the end of its life? Or a project where technology has to solve a real problem, not just exist? Let’s talk.