The migration that failed — and why it was the best thing that happened to our data

In this article:
A few years ago we had a simple problem. Information about a course curator was filled in manually on every landing page: photo, name, bio, workplace, each entered separately for every page and every language version. There was also a separate "program author" section. Often it was the same person.
When a curator changed jobs or updated their photo, someone had to walk through all the pages and fix it by hand. Keeping that kind of context in your head is impossible, so in practice almost nobody did it. Things got fixed only when someone pointed at stale information. And so we lived: one photo on one course, a different one on another, one job title here, another there, plus inconsistent transliteration of the name. One person, different content everywhere. The whole thing gave off the unmistakable whiff of a non-system.
On the surface it looked like a content problem. In reality it was an architectural one.

The symptom and the cause never live together
The observation that has fascinated me most over the years: the cause and the symptom of a problem almost never live next to each other. The symptom sat on the landing pages. The cause hid much deeper.
The easiest thing was to think at the feature level. Build a friendlier editing interface. Maybe a dedicated block in the admin panel, a template, or a new process for content managers. Each of those options patches the symptom and leaves the cause untouched.
Then one thought stopped me, and it changed the direction. The system had no concept of "this person." One human could be the curator of one course, a lecturer on another, the author of a third program, a mentor on the mentoring platform, a speaker at an event. But in the system she lived as dozens of scattered text blocks with no connection between them.
"This wasn't a content problem. It was a missing entity."
So the first thing we did didn't touch the interface at all. We created tutor as a separate, fully fledged entity in the system.
Naming as an architectural ceiling
The entity's name determined everything that happened next. We could have called it curator, since the original task was about course curators. A logical, narrow choice.
But "curator" is only a role. And in our ecosystem one person holds many roles at once. So I laid down a wider model:
- tutor. A person who passes on knowledge within the ecosystem.
- Role. Who they are in the context of a course: curator, lecturer, program author. These roles are not mutually exclusive.
- Context. Connections in other directions: mentoring, the library, events, community.
The name curator would have covered exactly one scenario. We would have locked in the architectural ceiling on day one. Good naming at the start works like strategy: it defines how the entity will be understood and reused later.

The migration that ran the audit for us
Creating the entity is the smallest part of the job. The moment it was ready, the question came up: where does the data come from? This is where everyone designing a system for the first time stumbles. The euphoria of "the feature is done" quickly gives way to sober reality: without data, nobody has any use for it.
The psychologically safest option would have been to sit people down to manually move curators from the landing pages into the new admin panel. But that would have treated a systemic problem with even more manual work. And it would have pushed colleagues away from the very solution meant to help them.
So we took the work on ourselves and did the engineering heavy lifting. The plan was simple and guaranteed no clean data, but it removed the grunt work:
- Step 1. Pulled the raw content from the web pages.
- Step 2. Found patterns in the text fields.
- Step 3. Parsed the data and split it into columns.
- Step 4. Used basic spreadsheet capabilities plus GPT-3.5 to assess data quality and find duplicates.
- Step 5. Deleted the obviously low-quality duplicates.
- Step 6. Imported the result as the initial fill.
And then the most interesting thing happened. The migration failed.
It was the best thing that could have happened. The script ran the audit for us: it ran into cases where one person had duplicates within a single language, with different bios, photos, and versions of the name. The script didn't know which version to write as canonical, so it stopped and logged everything it couldn't process. A human then muscled that list over the line.
That gave us a two-layer safety net: [human + AI] and [migration rules + logs]. The migration surfaced the problem far faster than people would have done by hand. And we involved them minimally, only at specific stages.
There was also a path that looked simpler: dump the data in as is and clean it inside the interface. A valid approach too. But it only works well with bulk actions and profile merging, and building those on day one is over-engineering. The engineering-driven migration delivered the same cleanup cheaper, with the quality audit as a byproduct.
The most dangerous moment is when the feature already works
After the migration we had normalized data and the first live source. Together with the courses care team we set up the process: tutors get created, linked to courses, updated, and deactivated. A data layer across courses, people, and roles came alive. We got our first statistics and data fit for AI.
And this is exactly where the trap hides, the one in which most infrastructure projects quietly die. We had covered the broad need: the data was accessible, the connections existed. But the original pain, manually filling landing pages, hadn't gone anywhere yet at that point. For managers, the new process looked for a while like process for the sake of process.
When people don't feel that a solution makes their work easier, they start neglecting it. Adoption fades gradually and invisibly. And then you pay twice: relaunching everything and backfilling the data nobody entered on time.
The lesson is simple and unpleasant. You can't let this stretch drag on. You have to recognize it early, plan the sequence and the resources, talk everything through openly with everyone it touches. And then be patient until the value becomes tangible.
How one entity grows connections
There was one way out of the trap: make the entity genuinely useful. Stop at a normalized directory, and tutor remains a pretty table. The real value began where it became the point of intersection with the rest of the ecosystem. Every new connection made the construction sturdier.

Mentoring: when a session becomes a connection, not a row
The first big intersection was the mentoring platform. I call it a "product built next door": conceptually it's about the same thing as courses, technically it lives in another universe, on Webflow with a booking module. A mentor is the same person carrying knowledge, just differently. A mentee is essentially the same student.
The simplest solution: catch a webhook for every session booking and drop it into a separate table. Two hours of work, seemingly done. But what would we get? A row saying "Viktor Ya. booked a session with Viktor Yu." The system knows a session happened, but doesn't know who these two are in the Projector ecosystem. Both exist as text, not as entities.
We built the sync through Make: the moment a new mentor appears on the platform, the system maps them against the database by a set of attributes. If the person is in the database, the profile gets enriched. If not, a new tutor is created and the data lands in our DB without a single manual action.
The value here isn't in yet another table. Mentoring became several things at once: the most accessible automatic source of new tutors and people data, a source of new student leads, a context of intent for everyone booking a session, and identified mentors and mentees, which is pure gold for marketing and operations. This is where the ecosystem started taking on the shape of a single source of truth. And a session turned from a row of text into a connection: student → session → tutor.
Landing pages, the catalog, and SEO on people's names
Once there was enough data, we switched the course landing pages from manual content to dynamically pulled profiles. That used to be non-trivial: content was assembled into JSON, from which the HTML page was generated, and there was no easy way to pull backend data into it. So for a while the old and new sections lived in parallel, and course managers verified the pulled data and edited the tutor profile itself instead of a separate page.
Since then, any change (photo, bio, workplace, name) updates instantly everywhere there's a connection. On the same entity we built the tutor catalog: over a thousand pages with unique content about real people. Name, photo, bio, social links, courses where the person teaches or is an author, and the option to book them as a mentor.
Then came an unexpected effect: SEO on people's names. Search for, say, Stas Hovorukhin, and next to LinkedIn a Projector page shows up in the results. The visitor sees which courses he teaches, that he's a mentor, and can book a session or take a closer look at a course. Every tutor card on a course landing became clickable, and we got internal linking between courses and profiles.
The same mapping we built for the sake of data unlocked one more thing: plugging the online booking widget for mentoring sessions straight into the Projector website. The mechanics didn't change; they just worked a second time. By then Projector had become the source of truth with identifiers across all systems, and that turned out to be enough for the same widget that ran on the mentoring platform to work on the tutor page too.
The content didn't appear in a single release either. First we pulled in the average price of a mentoring session, then a specific description of what the person can help with. The profile matured gradually.
And that opens the next step. With statistics in hand, we can work on tutor page v2: showing not just who this person is, but what they've done. How many students they've taught, how many courses and groups they've run, how many mentoring sessions they've held, how much they've raised in donations.
The blog, events, and ambassadors
From there, the same logic repeated on every new feature. We were moving the blog and knowledge base from Webflow into the system. We could have made the article author a text field again and repeated the old mistake. We did it differently: an article's author is a tutor. Now articles appear on the person's profile, and the blog gained connections to courses and mentoring.
Then came events. We introduced a rule: a speaker at an event is always a tutor in the system. An actual rule — no exceptions, no workarounds. Every new person who speaks automatically enters the graph and enriches it. The ambassador flag appeared the same way, for people who run local events. And it lives in that same profile.
If someone stops being an active mentor, the profile doesn't disappear. Only the specific module gets deactivated. The entity stays stable while roles and connections change freely. That's exactly why it's nearly impossible to break: it rests on many processes at once.
Data waiting for AI
Once we linked tutors to groups as well, an analytical model grew on top of the content. Questions that used to live in separate tables can now be answered by a machine: who ran a specific group, which students studied with whom, how many groups a curator has seen through to graduation, how many mentoring sessions they've held, how long they've been in the system.
This class of data underpins internal analytics: a tutor's personal statistics on their page, course performance, mentoring session activity in BigQuery. The data is normalized, connected, historical. Exactly the format needed for AI analysis of the whole business. Today, tutor intersects landing pages, the mentoring platform, the blog, events, the catalog, and analytics — seven-plus parts of the product. Library videos will join soon.
Three questions for any new entity
The framework I took away from this case is simple. Three questions worth asking before designing a new entity.
- Who else consumes this? If the answer is "only this module," you're probably looking at a feature, not an entity. If it's three different teams in three contexts, you have a node you can build around.
- What could attach to it in the future? You don't have to build everything at once. But the architecture shouldn't lock the doors in advance. If you can't picture how the next feature would connect to the entity, something's off with the schema.
- Does anything break when it grows? A well-designed entity doesn't break under additions — it gets stronger. Every new connection makes it more valuable.
There's an honest part here that matters. An entity without data is useless, and data won't appear without adoption. We deliberately started accumulating it in advance, before the teams saw practical value. The separate challenge is to communicate the value up front without becoming a windbag, and to muscle the scope over the line.
If you feel that something's "off" in your system, that data gets duplicated, that one person exists in several places as different text, that every new feature drags another manual process behind it, then maybe the issue isn't the interface or the content.
Maybe your system has a missing entity. Find it, design it right, and it becomes the point where everything that comes next intersects.
grow your careerwith real-world knowledge
data migrationfrequently asked questions
- What is a real master data management example?
- What is a single source of truth and why build one?
- How is a data entity different from a plain text field?
- Where do you start designing a canonical data model for a new entity?
