When two systems inside one company are merged, the thing everybody plans for is the collision: a table, an address or a setting that both halves own. Collisions are the easy half. They are loud, they can be counted before a line moves, and an address two files try to serve does not build at all.
This week a learning core built inside the group moved inside VARGATES Medical, the group's clinical simulation platform. Not one of the failures that reached a person was a collision. Every one of them was an agreement: two halves that were each internally correct, using one word and meaning different things by it, with nothing raising an error anywhere.
The loud half was decided first
Seventeen table names are owned by both lineages — sixteen real tables, plus the one both halves keep their migration bookkeeping in. Four page addresses existed on both sides. All of that is visible before anything moves, and all of it was decided first; we published that decision yesterday.
What follows is the other half, which nothing had planned for.
The same name, two meanings
At least eight environment variables are read by both halves under one name. On three of them the halves disagreed about what the value meant — and each half was right about itself, so nothing complained.
| The fact | What this platform means by it | What the arriving half meant | Where it fired |
|---|---|---|---|
REDIS_URL — whether there is a Redis at all | Empty by default: there is no Redis. Rate limiting runs in memory and never raises. | A URL by default: Redis is on localhost — which, inside a container, is nothing. | On every environment that does not set the variable, where signup answered 503. Production sets one, so no customer met this; it was found by a probe. |
CORS_ORIGINS — the origins a browser may call from | A string, split on commas. | A list — which the settings library reads from the environment as JSON. | The deployed value is not JSON, so constructing the settings raised and the arriving half failed at boot — caught in the release stack, before the surface first mounted. |
MINIO_ENDPOINT — the address of the object store | A bare host and port, which its client requires and which rejects a scheme. | The same string handed to a client that requires the opposite and refuses it before any network call. | On production. Every upload and every signed link answered 500 — and with uploads closed a course could not be published at all, because publishing refuses without a preview image and uploading one is the only way to get it. |
The Redis line is worth reading twice, because nothing in it malfunctioned. The arriving half's anonymous funnels fail closed on purpose: an anonymous write that also dispatches email must not become an unbounded flood while Redis is down. The 503 was the designed behaviour, correctly triggered — by a Redis that was never meant to exist. And it surfaced as a connection error against localhost:6379, which reads as a broken Redis rather than as a configuration decision. That half had no way to say “there is no Redis”, because its default was a URL rather than an empty string. It has one now.
The mail that was never sent, and said it was
Registration on production was a dead end, and the response both said so and denied it in the same breath:
{"detail": "verification email sent",
"email_verified": false,
"email_sent": false}Two mail transports in one product. This platform mails over an HTTP API. The arriving half looked for an SMTP host, found the empty default, logged the skip at INFO and returned false. In isolation that is defensible — declining to send when you have nothing to send with is correct. It became an outage because nothing anywhere asserted which transport this deployment actually has.
The computed field was right. The sentence beside it was a default that no send path ever revisited, and the sentence is the half a person reads. It is now derived from the field rather than passed alongside it — the difference between fixing a defect and preventing one, since it arrived as a default nobody set, and a rule of the form “remember to update both” would never have caught it. The deployment also reports which transport it holds now, so the question can be asked without sending anything. That was the whole cost: not that the answer was wrong, but that there was no way to ask.
Then the mail went out, to a laptop
With the transport fixed, the same walk was repeated a few hours later, and this time a verification mail arrived. It contained this:
http://localhost:3000/verify-email?token=…This platform calls the site's public address FRONTEND_URL. The arriving half calls the same fact FRONTEND_BASE_URL. Nothing set the second, so it kept the source project's development default — and every link that half puts in front of a person reads it: verification, password reset, the certificate code printed on a diploma and on a protocol, account activation, two nudge series, the unsubscribe page, course links, and the sign-on and messenger redirects.
Both settings were declared, non-empty and valid. Every check passed. The token in that link was good — verifying with it through the API returned a session — so the account was reachable by everybody except the person holding the mail.
And this is the shape that the obvious method cannot find. The three settings above share a name and disagree about the value, so a census of the names both halves read finds them. This one is invisible to that census because the names differ — which is why the census that now exists asks the other question instead: it enumerates every setting in the arriving half that still holds a development default, and demands an account of what happens on a deployment that never sets it.
Two doors into one product
Four addresses answered 200 — two belonging to this platform, two to the arriving half — in the bare form and under a locale prefix alike. Two user stores behind them, two different keys in the browser.
Nothing collided. The names differ, so the build was clean, every address resolved and every guard passed. It was visible only to a person who opened the site and was offered two different ways into one product.
The sharper half is what that cost. The site linked exactly one of the two, and nothing on the public pages linked the other — measured by searching the whole source outside the ported tree, not assumed. So the first step of what the port is for, a new person registering, could not be reached by clicking anything on the site. Only by typing the address.
/login 308 → /sign-in
/register 308 → /sign-up
/sign-in 200
/sign-up 200
/ru/login 308 → /ru/sign-inA permanent redirect rather than a 404, because a 404 spends the inbound links and bookmarks those addresses have accumulated and a redirect moves them. The old pages are moved to an archive rather than deleted, because the old layer has to outlive the new core's first live day.
The redirect deliberately drops the query string, so every internal link that carried one was repointed at the archive instead, where the old form still reads it. That is the part worth reading rather than skimming: those calls to action carry a role that the old form consumes and the new one has never heard of. Repointing them at the new door would have dropped it silently — and one of those links is the enrolment address a professor hands to a class.
The link that resolved, to the wrong system
Certificate verification exists on both sides, and the address belongs to this platform: it reads this platform's own certificates. Four pages of the arriving half's cabinet were sending their own certificate identifier there.
Every learner who clicked “verify” was told that the certificate was not found or the link was invalid — about a certificate that exists, in a sentence that is hardcoded English, so a Russian or Kazakh learner got it untranslated as well.
Two guards already watched this area and neither could see it. One asks whether this platform links into the ported tree, which is the opposite direction. The other asks whether a destination resolves — and for a name both lineages want, resolving is the symptom, not the proof: the route is real, the build is clean, nothing returns 404, and the reader is simply somewhere else. On a colliding identifier that is a plausible wrong answer instead of an error.
The guard that now exists asks the question that was missing: a destination named by the ported tree must resolve to a route that tree owns. Ownership is read from the file path, and exactly one shared destination is declared in writing — the site root, because there is one product and one front door.
How the ones that reached a person were found
Not by reading the diff. Every defect above that a customer could have met was found by walking the product on the live deployment, in the order a customer would: register, receive the mail, verify, create an organisation, invite a second person, create a course, publish it. One of the fixes records the method in a single line — found by trying it, because nothing else could.
That is not a criticism of the tests. It is a property of the shape. A suite that renders the page passes, because the page is right. A suite that exercises the endpoint passes, because the endpoint is right. What is wrong is the seam between two halves that are each correct — and a seam belongs to neither of the two people who would write the test.
If you are the one evaluating a supplier
Two of anything becoming one is a common event in a supplier's life, and it is usually announced as a capability. The useful questions are narrower than “did it go well”:
What happens when both halves agree on a word and disagree about what it means? Collisions are planned for. Agreements are not, and they fail silently in the direction that reads as success.
How did you find out? “The tests were green” and “somebody registered, published a course and read the mail” are different answers, and only the second one finds this class.
What is still not guarded? A supplier who can name the class it cannot yet catch has looked. One who reports only what it fixed has not been asked.
VARGATES Medical is at medical.vargates.com, and what it is built for is on its page here.