
- 3,704
- leads moved and verified
- 62
- custom fields audited on one table
- 6
- custom modules in source control
- 35
- dead views retired, one by one
- Client
- OrbitCare
- Industry
- Education and care staffing
- Location
- Dubai, UAE
- Year
- 2026
- Services
- CRM & ERP, Migration, Website
OrbitCare place teachers, teaching assistants, SEN specialists, nannies and domestic staff with schools and families across the UAE, and handle the visa process that goes with it. Every placement carries documents, approvals, timelines and a person’s residency status. The system tracking that has to be right. Theirs had drifted a long way from right.
Why it went wrong
They were on Odoo’s SaaS tier, which does not permit custom code. So every change the business needed had to go through Odoo Studio, the built-in click-to-configure tool.
That constraint is the whole story. Studio customisations live as records in the database rather than files. They cannot go in version control, cannot be code-reviewed, cannot be tested before they go live, and do not reliably survive an upgrade. There is no record of who added what or why. Every change lands straight on production, because there is nowhere else for it to land.
Do that for a few years, across several sets of hands that never spoke to each other, and nobody can safely change anything, or explain what is already there.
What we found
We cloned production into a sandbox and audited the whole thing before touching a line of it. What came back:
62 custom fields on the leads table alone. We measured how many records each one was actually filled on. The median field was populated on 3.6% of records. More than half, 28 of the 55 we could measure, were filled on under 5%. One field for ABA therapy licence numbers had been filled 20 times out of 3,704.
Eighteen custom models, seven of them completely empty. Built, abandoned, never removed. One had 38 fields defined and zero records in it. 35 of the 64 Studio views were attached to models nobody used.
And the same information in two places. Someone had added a field for a candidate’s ethnicity. Later, someone added another one. Both were live, both were populated: 2,376 records in the first, 108 in the second. Same for location: 2,889 records against 71. Same for job role, except that one was worse. 2,709 in one field and 1,826 in the other, both in active use, with no way to tell from the outside which was the truth.
That is the real cost of building in a click-tool. Not that it is untidy. That the answer to "what job role is this candidate looking for?" had two different sources and no tiebreak.
What we did
Built the replacement alongside the original. Rather than a big-bang rewrite, the new modules ran in parallel with the Studio layer while we proved each piece matched. Six custom modules: recruitment, client CRM, recruitment automation, applicant merging, an appointment API for the website, and mail delivery. All in source control, all reviewable, all deployable to staging first.
Decided with evidence, not instinct. The fill-rate audit is what made it safe to delete things. A field used 20 times gets a conversation; a model with zero records in it does not. For the duplicated fields we worked out which column was canonical case by case, and wrote merge logic for the one where both were genuinely in use rather than picking one and losing data.
Rehearsed the cutover before running it. Sandbox clone, acceptance tests, fresh backup, then a timed migration script: data moved, ownership of every configuration record transferred from Studio to the new modules, dead views and access rules deactivated, verified against a baseline. 35 views and 19 access rules retired, checked one by one.
Kept the history. 127 of the 3,704 leads were archived. Migrations that quietly drop inactive records are how a company loses its own past, so those came across flagged rather than discarded, along with 32 leads that had no company assigned and would otherwise have vanished.
Moved them off SaaS. Odoo 19 Enterprise, self-hosted on high-availability infrastructure we run. That is the change that makes all of the above possible. The tier they were on does not allow real code, so no amount of care would have fixed this while they stayed on it.
Built a new website. A Next.js site covering candidate and employer journeys, live vacancies, the four service lines (school staffing, SEN, childcare and domestic recruitment) and the visa process, booking interviews directly through the Odoo appointment engine rather than a disconnected form.
The scale of it
- 3,704
- leads
- 1,989
- applicants
- 52
- live job postings
- 81
- mail templates
- 14
- WhatsApp templates
All of it moved, reconciled and verified.
What it changed
A change to OrbitCare’s system now has an author, a review and a staging run before it reaches production, and it survives an upgrade. Every field has one canonical column. The models nobody used are gone, and the ones that matter are documented in code the team can read.
They run Odoo 19 Enterprise on infrastructure that allows real code, and the website books interviews straight into it. When the business needs something new, the answer is a pull request, not a workaround.
Inherited a system nobody can safely change? We have untangled a few. Tell us what you are working on.

