Refactoring a Legacy CRM without Downtime: 80% of the Codebase in 6 Months
In 2022, a US company [name under NDA] partnered with MobiDev to fix occurring issues in their CRM. We ran an audit that showed deeper underlying problems with the system. Built on legacy CodeIgniter 3.1 and PHP 7.1, it had critical security vulnerabilities that put daily operations at risk.
We proposed incremental modernization: refactoring and upgrading the application module by module. This approach allowed us to fit the client’s deadline while solving core architectural problems.
MobiDev’s team worked on the client’s system without disrupting its operations. We managed to upgrade 80% of the codebase in just 6 months (from PHP 7.1 to 8.1, from CodeIgniter 3.1 to 4), meeting the client’s deadline, eliminating security risks, and stabilizing the system performance.
The Story Behind Modernizing a Legacy PHP CRM
The client’s CRM and mobile app ran on CodeIgniter 3.1 and PHP 7.1. By 2022, the system had accumulated errors.
We started with a code audit instead of a fix. The errors turned out to be symptoms. The framework version had no end-of-life support, which created security risks and blocked environment updates. The accumulated tech debt and fragility of legacy code created multiple errors in the system. The bug fixes wouldn’t fix the issue of system instability. We listed the risks that would remain if the client stopped at bug fixing, so the decision could be made against evidence rather than a hunch.
That left the obvious alternative, a rewrite, which did not fit the date the client was already committed to. So we brought back a third option: divide the functionality into modules, rank them, and migrate the application to a supported framework version gradually.
Business Value of Refactoring the Legacy CRM
What the client got out of it, starting with the constraint everything else was measured against:
The original deadline held. The refactoring plan was built around the date the client already had, not offered as a replacement for it.
About 80% of the codebase upgraded in six months, with the CRM in production the whole time
The cause was removed, not the symptoms. The application moved off an unsupported framework version.
Infrastructure stopped limiting the code. Moving from an expensive, underused dedicated server to AWS, with the database on RDS and media files on S3, separated data storage from the application layer. This made horizontal scaling and CI/CD deployments possible while cutting hosting costs.
Less manual work in the infrastructure. The old setup relied on manual FTP deployment, which was replaced with an Ansible-based CI/CD pipeline. AWS added built-in backups and data protection.
Project Scope of the Legacy CRM Refactoring
Four to eight experts, depending on the phase, starting with consulting and continuing as a dedicated team.
Stage 1. Code audit. A full review of the existing source code before any upgrade work, which produced the timeline estimate and the verdict that the unsupported CodeIgniter version had to go. It also produced the plan: functionality divided into modules and ranked by priority.
Stage 2. Gradual refactoring. The size of the project and legacy code issues that had been left by the previous development team meant every feature had to be understood before it could be changed. About 80% of the code was upgraded in six months.
Stage 3. Cloud migration. The monolithic architecture made scaling server load difficult, so the database and file storage moved to AWS RDS and S3. That also replaced manual FTP deployment.
Stage 4. Growth. MobiDev team continued to focus on scaling the app, adding new features, enhancing existing ones, and ensuring smooth, error-free operation.
Deliverables of the CRM Modernization Project
Deliverables text:
- Source code audit results with prioritized task plan, module breakdown, cost estimates, and delivery timelines.
- Roughly 80% of the codebase updated to a supported framework version.
- Modular restructuring of application functionality.
- Database & File storage migration to AWS.
- Replacement of manual FTP deployment with an automated CI/CD pipeline.
- QA coverage across the refactored modules.
- Post-modernization feature development on the upgraded codebase.
Tech Stack
CodeIgniter 4
Redis
S3
EC2
RDS MariaDB
Firebase
Twilio
OAuth2
CodeIgniter 3.1 → 4
CodeIgniter 4
Redis
S3
EC2
RDS MariaDB
Firebase
Twilio
OAuth2
CodeIgniter 3.1 → 4
Modernize Your Legacy Product
Fill out the form and share your vision for your product. Our experts will get back to you within 1 business day.
FAQ
Bug fixing is the right call when your framework and language versions are still supported and the failures are isolated. When the framework is out of support, individual fixes leave the cause in place and the error count keeps climbing, so you pay repeatedly for the same problem. A code audit is the cheapest way to find out which situation you are in, and it should come back with the specific risks that remain if you patch instead of refactor. That is what happened here. The audit found an unsupported framework version, and the remaining risks were written out for the client before any decision was made.
Usually yes, if the functionality can be broken into modules and ranked. The application keeps serving users while modules move one at a time, which avoids both a feature freeze and a single high-risk cutover. It is slower per module than starting over, and it requires working out what the existing code was meant to do, which is the part people underestimate. This CRM was migrated that way, with about 80% of the codebase upgraded in the first six months.
Sometimes, and an audit is what tells you before you commit. Fitting a fixed date means deciding what migrates now and what waits, which needs modules ranked by risk and dependency rather than a codebase treated as one indivisible thing. If a vendor answers a deadline question without reading your code first, the number is a guess. On this project the deadline existed before we were involved and the plan was built around it.
The size of the codebase matters less than three other things: how interconnected the code is, how much of the original intent is documented, and how much of the system has to keep running while you work. Interconnection is usually the biggest multiplier, because it decides whether modules can move independently or drag each other along. Undocumented intent is the second, since someone has to reconstruct what each feature was supposed to do before touching it. An audit prices those honestly. A vendor quoting without one is pricing their optimism.
In a gradual migration the data usually stays where it is until there is a specific reason to move it, which keeps the risk surface small. When storage or the database does move, that is its own stage with its own rollback plan rather than something bundled into a code release. Here the database and file storage moved to AWS RDS and S3 as a distinct stage of the project.
Often it does, and it usually has to move first. Self-managed servers and a monolithic architecture limit which framework and runtime versions you can support, so infrastructure work tends to be a precondition for code work rather than a separate project you can defer. Manual deployment is the other common blocker, because it makes frequent small releases risky, and frequent small releases are how gradual refactoring stays safe. Here the database and file storage moved to AWS RDS and S3, and manual FTP deployment was replaced with an automated CI/CD pipeline.
No, and jumping straight to microservices during a modernization is a common way to turn one problem into two. A monolith on a supported framework, deployed automatically, is a perfectly reasonable place to stop. The question worth asking is which specific constraint is hurting you, because that is usually solvable on its own. On this project, scaling server load was the difficult part, and moving storage and the database to managed cloud services addressed it without restructuring the application.
Your team knows the domain, which is the expensive knowledge. An outside team brings two things harder to supply internally: people who have seen this specific framework migration before, and the willingness to tell you the answer is a rewrite if one is really necessary. In-house teams under feature pressure rarely get permission to stop and modernize, which is how the debt accumulated in the first place. The workable arrangement is usually both, with the outside team owning the migration and your people owning the domain questions.
The team that just learned your codebase is normally the cheapest team to build features with, because the discovery is already paid for. Handing the work to a new group means paying for that learning twice. Here, the dedicated team stayed on for 2 years after refactoring closed, moving to scaling and new features.
You rarely need a full rewrite, but you do need a stack that can accept a modern SDK, and an unsupported framework version often cannot. The practical order is: work out what your current stack can physically support, put the model work where the data already lives, and modernize only the parts standing in the way. Sometimes that is one service. Rewriting everything first is the expensive way to find out you did not have to.
The ones that read data you already have rather than generating new content. Lead scoring, duplicate detection, churn signals, and summarising long records into something a person can act on all work off existing fields. Natural language search over your own records is usually the highest value per unit of effort, because the data is already structured and the win is immediate. The features that disappoint are the ones that write on your behalf without knowing your customers, which is most of what gets demoed.
Probably not yet, and that is the normal answer for any system that has been running for a decade. Legacy CRMs accumulate duplicate records, abandoned custom fields, and inconsistent data entry, and a model trained on that learns your bad habits. Before anything else, find out how much of the data is actually complete for the specific question you want answered, because you do not need clean data everywhere, only in the fields the model reads. That assessment is usually a week of work and it saves months.
Different in shape from the build cost, and the difference surprises people. Build is one bill. Inference is a bill every month that scales with usage, so a feature that works well gets more expensive as it succeeds. It is worth designing the cost model before the feature: which calls hit a model versus a cached result, whether a smaller model handles most cases, and what happens when volume triples. Getting that wrong is the most common reason an AI feature that worked in the pilot gets switched off later.
It helps most on exactly the parts of legacy work people dread, and least where you might hope. Reading unfamiliar code and explaining what a function appears to do, writing tests for code that never had any, and mechanical translation between framework versions are all real gains. What it does badly is decide what the code was supposed to do, and on old systems that intent is the actual work. Undocumented business rules cannot be inferred from the code that implements them, because the code may well be wrong. Use it to read faster, not to decide.
Usually, and more often than people expect once they have been told to start over. The typical pattern is a product that works for the demo path and falls apart on edge cases, with no tests, inconsistent structure, and security handled optimistically. That is a known shape of problem and it is cheaper to audit than to rebuild from nothing, because the business logic inside it is real even when the code is not. MobiDev runs a fixed-price audit for exactly this situation, which tells you what is salvageable before you commit to either path.
Same way you keep any code from becoming that: review it as if a junior wrote it in a hurry, because functionally that is what happened. The specific risks are code that works without anyone understanding why, dependencies added casually, and patterns that vary file to file because each was generated in isolation. A style guide the whole team holds to matters more when part of the code is machine-written, not less. Speed on the way in is what creates the debt.