The Prototype Was Working. I Rebuilt mpn.cc Anyway.
mpn.cc began as eleven apps, 22 GitHub workflows, a copied quote backend, and several competing sources of truth. This is how I deleted my way toward a smaller, evidence-backed catalog.
mpn.cc was live.
It could search electronic parts, show manufacturer pages, fetch prices, mirror datasheets, and run a surprising number of background jobs. If I typed an MPN into the search box, something useful usually happened.
It also had eleven applications, 22 GitHub Actions workflows, a local copy of another product's quote backend, a shared job table pretending to be several queues, and more than one answer to the question: “Where does the catalog actually live?”
Both descriptions were true.
The prototype had done exactly what a prototype should do. It let me explore the problem quickly enough to discover what the real product was. The mistake would have been treating that discovery vehicle as the final architecture.
This is the story of the three weeks after the prototype branch: what I deleted, what I kept, and how mpn.cc changed from a collection of promising features into a system I could reason about.
The Prototype Was Not a Failure
I want to be fair to the old system because it is easy to look at deleted code and act smarter than your past self.
The prototype taught me that electronic-component search is not one problem. It is at least four problems sitting very close together:
- identifying the exact manufacturer and orderable MPN;
- finding useful catalog and datasheet information;
- collecting live price and stock offers;
- deciding which claims are safe to show as facts.
I did not know where those boundaries belonged at the beginning, so I built through them. A new data source became a new worker. A slow task became a queue. A useful one-off operation became a GitHub workflow. The price service lived inside the repository because it was convenient. The catalog had relational tables, JSON collections, and memory-backed behavior because each had solved a local problem at a different time.
This was productive for a while. It was also how the system accumulated eleven directories under apps/:
api
web
data-engine
manufacturers-engine
suppliers-engine
farnell-params-engine
compel-catalog-engine
compel-publish-engine
partsdirect-manufacturers-engine
discovery-engine
enrichment-engineSome were real services, some were partially overlapping workers, and some were scaffolding for a future that had not arrived yet. The architecture document had a “current” column and a “target” column because even the documentation could no longer describe one coherent system.
That was the first useful warning.
I Had Built Several Businesses at Once
The technical cleanup did not begin with TypeScript, Fastify, React, or a database schema. It began with a business question:
What does mpn.cc own?
The answer I eventually wrote down was simple: mpn.cc owns canonical electronic-component identity and the evidence attached to that identity.
It should be able to say that a part is made by Texas Instruments, that its exact orderable MPN is LM747H/883, that it belongs to a specific category, and that a particular immutable PDF supports a particular technical value.
It does not own the entire procurement stack. Live price, stock, lead time, and distributor collection already belonged to Quote. Keeping a second Quote backend inside mpn.cc meant two implementations could disagree about providers, normalization, retries, and offer semantics.
So the first large architectural improvement was deletion: I removed the local quote stack and made Quote the only owner of quoting. mpn.cc kept a small Quoting Edge that knew how to call it, normalize the response, and keep credentials away from the browser.
That one decision removed an entire database, a large provider tree, deployment jobs, migrations, and monitoring concerns. More importantly, it removed an argument about ownership.
The same rule later shaped BOMPrice and other DANNIE tools: they can consume the component catalog, but they do not invent parallel component identities.
Deletion Was the Migration Strategy
mpn.cc was public, but it had no users or external API consumers at the time. That was an unusual and temporary advantage.
There was no reason to build compatibility layers for behavior I already knew I wanted to remove. I did not need dual reads, dual writes, feature flags, a parallel rollback stack, or a six-month strangler migration. I did need to preserve anything expensive or non-rebuildable, especially mirrored datasheets in R2 and carefully curated identities.
I wrote that distinction into an architecture decision:
preserve durable evidence and curated identity
delete rebuildable projections and abandoned behaviorThen I worked in small slices. Each slice had to preserve one product boundary while deleting more accidental machinery than it introduced.
The approach was not a rewrite. A rewrite would have changed behavior, persistence, deployment, and language all at once. Instead, I replaced one boundary at a time and kept the service running between changes.
Some days the most meaningful commit had more deleted lines than added ones. Those were usually the good days.
PostgreSQL Had to Become Boring
The prototype called itself PostgreSQL-backed, but that description hid an uncomfortable detail. Important production reads still passed through a memory store and JSON collections in app_state_records. Some routes read relational tables, some read projections, and some depended on which collections the API skipped during startup.
There was even a documented gap where catalog pages used PostgreSQL while part resolution could still be memory-only.
That is a dangerous property for a catalog. If two read paths can disagree about whether a part exists, they are not caches. They are competing realities.
I replaced the old migration chain and catalog read model with a fresh relational kernel. The central identity became:
canonical manufacturer + strict normalized MPNStrict matters here. Punctuation-insensitive normalization is useful for search, but it is not safe for merging orderable part identities. The original manufacturer spelling remains as source lineage; canonicalization does not erase what the provider actually sent.
The database also stopped pretending every record meant the same thing. A public part, a private candidate, a provider observation, an immutable datasheet, and an extracted claim became separate records with separate rules.
PostgreSQL became the only required application state. Redis became optional. JSON stopped being a second catalog. Memory stopped being a production fallback.
This made the database less clever and the product much easier to trust.
I Added TypeScript After Finding the Boundaries
At this point it would be tempting to describe the transformation as a JavaScript-to-TypeScript migration. The file counts are dramatic: the prototype had 195 .mjs files; the current branch has two. There are now 544 tracked .ts and .tsx files.
But renaming files was never the goal.
Typing the prototype's accidental interfaces would only have made the wrong architecture harder to change. I first decided who owned component identity, quoting, catalog persistence, provider acquisition, and datasheet evidence. Then I introduced strict contracts at those edges.
Untrusted HTTP, provider payloads, database snapshots, and model output begin as unknown. They are parsed once at the boundary. Internal services receive a type they can rely on. Stateful workflows use explicit unions instead of combinations of booleans that happen to work today.
The workspace eventually settled into three packages:
@mpn/contracts browser-safe schemas and projections
@mpn/api domain logic, Fastify, PostgreSQL, ingestion
@mpn/web React, Vite, public HTML, admin UIThe dependency direction is intentionally boring:
@mpn/contracts ← @mpn/api
@mpn/contracts ← @mpn/webThe web application and API do not import each other. The contracts package does not become a dumping ground for repositories, HTTP clients, credentials, or provider code.
Fastify and React were useful, but neither fixed the architecture. They arrived after the ownership decisions and had to prove that they removed more custom plumbing than they added.
Datasheets Changed the Meaning of the Product
The most important change was not a framework. It was deciding that a datasheet URL is not evidence.
A vendor can change a PDF behind the same URL. A distributor can remove it. A server can return an HTML block page with a .pdf path. If mpn.cc is going to publish technical data, it needs to know which bytes supported that claim.
The new flow downloads the document, checks that it is really a PDF, hashes the bytes with SHA-256, stores it under an application-owned immutable key in R2, and records the exact relationship between that document and the part.
Only then can analysis begin.
The model receives deterministic, page-addressed chunks from the mirrored PDF. It returns structured claims with page and chunk evidence. The application checks those references, the category profile checks the parameter names and units, and publication happens through a separate review boundary.
This distinction took several iterations to get right:
model output ≠ catalog fact
extracted claim ≠ published fact
approved PDF ≠ approved technical valueAn extracted claim says, “the model found this in these immutable bytes.” A published fact says, “the catalog accepted this claim under this versioned category contract.”
That difference now drives the admin interface, public part pages, reanalysis behavior, and retraction rules. It also explains why provider refreshes cannot overwrite a reviewed datasheet fact just because they arrived later.
The Admin Became Part of the Architecture
The prototype had operational endpoints and workflows. The newer system has governed decisions.
That sounds like a UI distinction, but it is an architectural one. A catalog administrator needs to see why a part is blocked, which organization identity was resolved, which source observation changed, which PDF page supports a claim, and what will become public after approval.
Bulk sources now produce immutable, provider-neutral import artifacts. Planning compares an artifact with the current database without mutating it. Applying a plan is bounded to explicit rows, revalidates the artifact and live identities, requires a reason, and writes an audit event.
On-demand provider acquisition follows a different path. It searches configured sources under one time budget, ranks exact identities, records observations, and may create a candidate or published part according to admission policy. It does not become a hidden bulk importer.
Datasheet review is separate again. A PDF can be approved while its claims remain evidence-only. A current analysis can be blocked while older published facts remain valid. These are not edge cases; they are the natural result of modeling catalog identity, evidence, analysis, review, and publication as different states.
The admin stopped being a remote control for background workers. It became the place where irreversible meaning is assigned.
The Infrastructure Got Smaller as the Product Got Larger
The prototype branch had 22 files under .github/workflows. GitHub Actions had gradually become an operations dashboard: start this worker, pause that backfill, diagnose a source, run a one-off fill, repair a deployment.
Today there is one deployment workflow.
The current architecture runs five always-on services on one VPS:
Cloudflare → Caddy → web
→ API → PostgreSQL
analysis worker → PostgreSQL
datasheets and images → Cloudflare R2The only background process is the bounded datasheet analysis worker. Catalog imports are explicit artifacts and commands. Datasheet mirroring is an explicit bounded operation. Redis is an optional profile, not a hidden requirement.
This is not the architecture I would choose for every possible future version of mpn.cc. It is the architecture the current workload has earned.
If analysis volume eventually requires more workers, or import preparation needs a durable queue, I can add them with evidence. I no longer deploy infrastructure because a diagram has an empty box labeled “future.”
Three Weeks Later
From the prototype branch on July 12 to the current branch on August 3, the repository moved through 375 commits. The raw diff is messy—1304 files changed, with about 180,000 insertions and 154,000 deletions—because the frontend, database, tests, and operational model all changed along the way.
The smaller comparison is more useful:
| Prototype | Current | |
|---|---|---|
| Application directories | 11 | 2 |
| GitHub workflows | 22 | 1 |
.mjs files | 195 | 2 |
.ts / .tsx files | 178 | 544 |
| Architecture decisions | 0 | 34 |
The ADR count is not a score. Documentation can become its own form of avoidance. In this case, the decisions matter because they explain why a future contributor should not casually restore the local quote backend, add another source of truth, let a model write facts, or deploy a worker for every command.
The product is larger now. It has a public catalog and API, governed imports, provider acquisition, immutable assets, datasheet analysis, fact review, cross-references, organization management, and live Quote integration.
The runtime is smaller because those capabilities finally have owners.
I Would Not Start With This Architecture
If I started mpn.cc again tomorrow, I would not begin with 34 ADRs, a category-profile system, an evidence ledger, and an admin review workspace. That would be architecture cosplay.
I would build another prototype.
The difference is that I would be more willing to throw it away.
The prototype was valuable because it exposed the real questions. Which identity is canonical? Which data is observation? Who owns pricing? What does “verified” actually mean? What survives when a provider changes its response? Those questions were difficult to answer before I had a working system and production data to push against it.
The architecture came from answering them one at a time.
The next test was much less philosophical: an 82 MB manufacturer archive expanded into 4.63 million rows and forced this design to prove itself under real load. I wrote about that in Importing 3.57 Million Electronic Parts Without Pretending the CSV Was True.
That import is where the new mpn.cc stopped being a cleaner prototype and started feeling like a real catalog.