Joomla
Joomla 3 to 5 rescue: inheriting a stalled federation site
The previous agency had been on the migration for nine months. The staging site was on Joomla 4.4. Production was still on 3.10. Nothing on either of them worked.

The federation's IT lead phoned us on a Tuesday in February. The previous agency had been on the Joomla migration for nine months. The staging site was on Joomla 4.4. Production was still on Joomla 3.10. Neither of them worked. The competition season started in six weeks, and 47,000 members needed to be able to log in, pay their dues, and see who was refereeing their match on Saturday.
The Akeeba Backup Professional subscription had expired in 2022. The custom referee-assignment module was written in 2014 by a developer who had since died. The MySQL database was 11 GB and ran on a shared host with a 30-second query timeout. The previous agency had left a Slack channel full of half-finished branches and a Trello board with 84 cards in "In Progress".
This is the post-mortem of how we got that federation onto Joomla 5, what we threw away, and what we kept. It is not a tidy story.
What we found when we opened the box
Day one was reading, not coding. We pulled a full mysqldump with --single-transaction, gzipped it, and dropped it onto a local Docker stack. Then we opened the admin panel of the staging site and started clicking. Within an hour we had a list of seventeen extensions that the staging site loaded but production did not, and eleven that production loaded but staging did not. The previous agency had been installing things on staging to fix bugs they had not yet reproduced on production.
The referee module was the one everyone was afraid of. It was a mod_ extension that read three custom tables, joined them against the core #__users table, and rendered a weekly assignment grid. The code was procedural PHP 5.3 idioms with a mosLoadAdminMenus call still in it. That function had been removed in Joomla 1.6. The reason it still worked was a polyfill that someone, at some point, had pasted into configuration.php. We found it on line 412.
If a legacy Joomla site "just works" but you can't explain why, open configuration.php and defines.php first. Polyfills hidden there will silently break under Joomla 4 and 5, and the resulting fatal error will look like an extension bug.
The three-step trap
The official path from Joomla 3 to 5 is two hops: 3.10 to 4.4 LTS, then 4.4 to 5.x. The previous agency had tried to do both in one weekend by editing the database directly. The #__update_sites table had been hand-patched. The #__extensions table had rows for components that no longer existed on disk. The #__assets table was missing the nested-set integrity that Joomla's ACL relies on.
We did not try to fix the broken staging site. We threw it away. The correct path, as Joomla's own migration documentation spells out, runs through the Pre-Update Check in the Joomla Update component. That check refuses to proceed if any installed extension is not flagged as J4-ready. The previous agency had bypassed it by editing the database directly. We started from a fresh copy of production and let the checker tell us the truth.
The truth was: of 43 installed extensions, 19 had a J4-ready release, 11 had been abandoned upstream, 7 needed paid upgrades, and 6 were custom-built for this federation and had no maintainer.
The Akeeba problem
Akeeba Backup Professional was the federation's backup tool. The subscription had lapsed in 2022. The free Akeeba Backup Core release is fine for most sites, but the federation was using JoomlaPack-era profiles with split archives, native S3 upload, and pre-flight database dumps that the free release does not include. Without those, a restore on a new host would have been a 40-minute affair, not the 6 minutes it currently took.
We did the sensible thing: we paid for a one-year Akeeba Backup Professional subscription on the federation's account, updated the extension, and ran a full backup before touching anything else. The board approved the €120 invoice in twenty minutes. Nine months of agency time had cost them more than that in a single morning of standups.
Before you debug a stalled migration, buy back the licenses the previous team let lapse. The annual cost of every Joomla extension subscription on a mid-size site is roughly €600. That is one hour of agency time.
Rewriting the referee module
The referee module was the one piece of code that could not be replaced by an off-the-shelf extension. It encoded the federation's specific business rules: a referee cannot officiate a match for a club they once played for; senior referees get first pick of regional finals; travel distance over 80 km triggers an expense claim. Eleven years of edge cases lived in a single 1,400-line file.
We rewrote it as a Joomla 5 module using the modern Joomla\CMS\Helper\ModuleHelper dispatch pattern, with a service provider, a DI-injected database driver, and the rules extracted into a plain PHP class that we could unit-test outside Joomla entirely.
<?php
namespace Federation\Module\Referee\Site\Helper;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\Database\DatabaseInterface;
final class RefereeHelper
{
public function __construct(
private readonly DatabaseInterface $db,
private readonly AssignmentRules $rules,
) {}
public function getAssignmentsForWeek(\DateTimeImmutable $week): array
{
$query = $this->db->getQuery(true)
->select($this->db->quoteName(['a.id', 'a.match_id', 'a.referee_id']))
->from($this->db->quoteName('#__federation_assignments', 'a'))
->where($this->db->quoteName('a.week_start') . ' = :week')
->bind(':week', $week->format('Y-m-d'));
$rows = $this->db->setQuery($query)->loadAssocList();
return array_filter($rows, fn(array $row) => $this->rules->isEligible($row));
}
}
The rewrite took eleven working days. We kept the original database schema because 47,000 members had years of assignment history in it, and migrating that data was a risk we did not need to take. Same tables, new code.
The eleven-gigabyte database
The MySQL database had grown to 11 GB. Two-thirds of that was the #__session table (Joomla 3 had never been configured to garbage-collect sessions) and the #__action_logs table (every login since 2017). We did not migrate those rows. We truncated #__session on the new instance and archived #__action_logs older than 18 months to a cold-storage table the federation could query if a disciplinary case ever needed it.
The actual member, club, and competition data was 3.8 GB. That fit comfortably under the new host's query timeout. We moved off the shared host onto a managed VPS at a Dutch provider in the same week, partly because Joomla 5 wants PHP 8.1+ and the old shared host capped at 7.4.
The migration weekend
The cutover happened on a Saturday between two competition weekends. The plan was four pages of bash and SQL. The rehearsal had taken 4 hours and 12 minutes. The live run took 4 hours and 41 minutes, almost all of the overage spent waiting for DNS to propagate to one stubborn ISP. We monitored the new site for 72 hours with a five-minute uptime check and a synthetic login that exercised the referee module every fifteen minutes.
Two bugs came in over the first week. One was a CSS regression in a third-party calendar component. The other was a missing translation string in the Dutch language pack. Neither blocked a single referee assignment.
What we would tell the next federation
Stalled Joomla migrations almost always look the same. A site stuck on 3.10 past its end-of-life date. An agency that tried to skip the 4.x intermediate. A custom extension nobody wants to own. A backup tool whose license expired. The fix is rarely heroic. It is reading the documentation, throwing away the half-broken staging site, and budgeting the boring days.
When we took on this legacy migration, the thing we ran into was the assumption that the previous agency's work was a foundation we could build on. It was not. We ended up solving it by treating production as the only source of truth and rebuilding the path forward from there.
If you are sitting on a Joomla 3 site this week, the smallest thing you can do today is open the admin panel, go to Components → Joomla Update, switch the channel to "Joomla Next", and read what the Pre-Update Check tells you. Don't fix anything yet. Just read the list. That list is the real shape of your migration.
Key takeaway
Stalled Joomla migrations are rarely a code problem. They are a documentation, license, and discipline problem dressed up as one.
FAQ
How long does a Joomla 3 to 5 migration actually take?
For a site with 30 to 50 extensions and a moderately customised template, plan on six to ten working weeks, including a rewrite of any custom extension older than five years.
Can you skip Joomla 4 and go straight from 3 to 5?
No. The Joomla Update component requires an intermediate hop through 4.4 LTS. Trying to skip it by editing the database leaves the asset tree and ACL in an inconsistent state.
What do you do with a custom extension whose author is no longer reachable?
Extract the business rules into a plain PHP class, write tests against them, then wrap that class in a fresh Joomla 5 module. Keep the database schema if possible to avoid data migration risk.
Is Akeeba Backup Core enough for a federation-size site?
For sites under a gigabyte, yes. For multi-gigabyte sites with split archives, native S3 upload, and tight restore-time targets, the Professional subscription pays for itself the first time you need a fast restore.
How do you handle the session and action-log tables in a Joomla migration?
Truncate the session table on the new instance. Archive action logs older than 18 months to a separate table you can query if a compliance case needs them. Don't migrate either table wholesale.