Case study · Automotive B2B platform
A server move became a Laravel 8 to 13 modernization.
The client wanted to move a mature B2B application to a new server. I could have transferred it unchanged. It would have run on new infrastructure, but still on an old framework, with aging dependencies and little protection for its most important workflows.
I proposed a controlled upgrade rather than a rewrite. The goal was to make the application maintainable again without reinventing ordering, stock, logistics, documents, or billing along the way.
Starting point
This was not an ordinary server move
The platform handles ordering, stock movements, logistics, documents, and billing. It also exchanges data with external systems. A defect does not merely affect an old administration tool. It can stop real work or produce incorrect business data.
During the assessment I found Laravel 8, PHP 8.0, Livewire 2, Laravel Mix, and several dependencies that blocked newer framework releases. The application also lacked enough automated protection for a responsible multi-version upgrade.
That changed the assignment. The server still had to move, but the move became the final step of a larger modernization.
The decision
Why I modernized the application instead of rewriting it
A rewrite would have looked cleaner. It would also have turned years of accumulated rules back into assumptions. Which order lines may be combined? When can an invoice be sent again? Which permission applies if access is revoked between two requests?
Rules like these rarely exist in one complete specification. They live in code, database structures, and the expectations of the people who use the system every day.
So I kept the business core. I replaced the technical foundation in controlled steps and changed domain behavior only when tests exposed a defect or an unsafe boundary.
The approach
First, the old application had to explain its behavior
Before updating Composer, I worked through routes, Livewire components, jobs, migrations, and the most important workflows in the running application. That investigation became tests, factories, and support code around its existing business rules.
This first phase added 10,423 lines. The number itself is not the point. What mattered was the behavior those tests captured: orders, permissions, cart behavior, documents, background jobs, and external handoffs.
The work also found defects that already existed on Laravel 8. I fixed them before upgrading, so a later failure would not leave me guessing whether the framework change had caused it.
Delivery
Five Laravel versions without one giant leap
I moved from Laravel 8 through each major release to Laravel 13. PHP tests, the frontend build, and browser flows had to work again after every stage. Only then did I move to the next version.
That may sound slower than one large jump. In practice, it cuts investigation time. When something breaks, the cause sits in a small change set instead of somewhere across five framework releases and a completely different frontend build.
The modernization remained understandable across 35 focused commits. Each step had one clear job and a result I could verify.
| Area | Before | After |
|---|---|---|
| Backend | Laravel 8 · PHP 8.0 | Laravel 13 · PHP 8.3 |
| UI | Livewire 2 · Tailwind 2 | Livewire 4 · Tailwind 4 |
| Build / Tests | Mix · PHPUnit 9 | Vite 8 · PHPUnit 12 |
The difficult parts
The real work happened between framework upgrades
Laravel itself was rarely the largest problem. The harder work sat where the old system carried assumptions nobody had written down.
An abandoned cart package blocked the upgrade
I neither copied the entire package nor rewrote checkout at the same time. The application took ownership of the small contract it actually needed, including cart instances, items, options, events, totals, and stored carts. Existing tests preserved its behavior.
Livewire turned browser state into a security concern
I treated public properties as request data. The server recalculated prices, checked record ownership again during saves, and denied access that had been revoked between requests. Render tests would not catch these failures, so the suite exercised complete actions.
Background jobs had to survive repetition
A repeated request could not create a second order. A worker restart could not deliver the same document or email twice. Persistent status records, unique jobs, and locks made those operations safe to retry.
Passing PHP tests do not prove the production build works
During the move from Mix to Vite and Tailwind 2 to 4, dynamically assembled classes could disappear from the final CSS. A build check now looks for 22 required selectors in the generated file. I also replaced 124 external icon-component usages with local SVGs and removed 17 Composer packages.
Going live
The code was not done until it ran safely in production
The upgraded application first ran in isolation on the new runtime stack. Anonymized data made real business workflows testable. Mailpit captured outgoing messages so a test run could not reach a customer.
Before the switch, I checked the scheduler, queue, public entry points, six browser flows, Blade compilation, Composer audit, and the production build. Client acceptance was followed by a data freeze, final database migration, and domain switch.
On the new host, I checked authentication, ordering, documents, email handoffs, the queue, and the scheduler again. Only then did I consider the modernization complete.
The result
What the client received
The client did not receive a new application that had to relearn how the business works. The familiar workflows now run on a current technical foundation, with much stronger protection for future changes.
- Laravel 8 → 13
- Moved to a supported framework release
- 698 PHP tests
- With 4,593 assertions around business behavior
- 6 browser flows
- Checked against the real production build
- 17 packages removed
- Fewer dependencies in the running application
The application now runs on Laravel 13 on the new host. Its tests document more than whether pages render. They preserve the business workflows that the next change must not break.
Looking back
What I took away from this modernization
I would again start with behavior, not the target version. Laravel has a documented upgrade path. The harder job is understanding a mature application well enough to know what may change and what must remain untouched.
I would also move through one major release at a time again. Large jumps look shorter in a project plan. Small steps that are fully checked are usually faster in a business-critical application because failures have a clear origin.
And I would again treat the production switch as part of development. A modernization does not end with green CI. It ends when people, jobs, and integrations continue to work reliably on the new system.
My role
I planned, delivered, and took the modernization into production
I am Mathias Onea, a software engineer focused on Laravel, PHP, and mature business applications. On this project I was responsible for the assessment, upgrade strategy, tests, security fixes, frontend migration, and production switch.
The figures on this page were checked against the final upgrade branch and its successful CI run.