Skip to content
Go back

Upgrading to Laravel 13 with Laravel Shift & Laravel Boost

Laravel 13 launched on March 17, 2026. The official upgrade guide estimates 10 minutes. It took me a full session — not because the framework changes were hard, but because of dependency blockers, Algolia removal, and a couple of Shift bugs.

The interesting part wasn’t the upgrade itself. It was the workflow: using two different AI-powered tools together, each doing what it’s best at.

Why both Shift and Boost?

Laravel Shift ($19) and Laravel Boost (Laravel’s first-party MCP server for AI coding agents) approach upgrades differently.

Shift’s goal is to give you an app that looks and feels like it’s always been written in Laravel 13. It adopts new conventions, modernises syntax, converts to PHP attributes, and applies code style changes. It works like a senior developer who’s read every line of the upgrade guide and the framework changelog.

Boost’s goal is context-aware compatibility. It has access to your database schema, routes, models, installed packages, and error logs via MCP. It can reason about your specific app — what cache driver you’re using, whether you store PHP objects, which env vars are set in production.

The two complement each other. Shift handles the mechanical changes reliably at scale. Boost handles the judgement calls that need app-specific context.

Laravel ShiftLaravel Boost (Claude Code)
ApproachDeterministic automationContext-aware reasoning
StrengthConvention adoption, syntax modernisationBreaking change analysis, dependency investigation
OutputPull request with atomic commitsInteractive walkthrough with explanations
Best atThings that can be reliably automatedThings that need human judgement
Cost$19 per upgradeIncluded with Claude Code

The approach: Shift + manual review + Boost check

Step 1: Run Shift — it opened a PR with atomic commits for every change, plus emoji-coded comments (❌ must fix, ⚠️ should review, ℹ️ informational).

Step 2: Feed everything to Boost — I pointed Claude Code at the Shift PR and walked through each comment together. For every warning Shift flagged, Boost could check the actual codebase to determine whether it applied to my app.

Step 3: Verify against the official upgrade prompt — at the end, I pointed Claude Code directly at the Boost upgrade prompt source to cross-check we hadn’t missed anything:

check out the laravel boost upgrade-laravel-v13 prompt and tell me if we’ve missed anything?

It checked all the breaking change categories against the codebase — CSRF middleware references, JobAttempted event listeners, QueueBusy properties, Container::call usage, DELETE queries with JOINs, pagination views, Js::from in tests, custom cache/queue drivers, manager extend() calls, UUID factory resets. None of them applied to my app. Clean bill of health.

The breaking change that matters most

The single most important thing in the Laravel 13 upgrade has nothing to do with code. It’s environment variables.

Laravel 13 changed how CACHE_PREFIX, REDIS_PREFIX, and SESSION_COOKIE are generated from your APP_NAME. The old format used underscores (growth_method_cache_), the new format uses hyphens (growth-method-cache-).

If you don’t explicitly set these in your production .env, upgrading will silently invalidate all cached data and log out every user because the session cookie name changes.

is the biggest risk here the dependencies?

No. The biggest risk is deploying without pinning your cache and session prefixes. I added these to .env.example and production:

CACHE_PREFIX=growth_method_cache_
REDIS_PREFIX=growth_method_database_
SESSION_COOKIE=growth_method_session
SESSION_SECURE_COOKIE=true

The formula for the old defaults is Str::slug('Your App Name', '_') — run that to figure out your current values before you upgrade.

What needed manual review

Shift handles the straightforward changes well. The items below are the kind of thing that need a human eye (or an AI with full codebase context) — places where the automated change was reasonable in isolation but didn’t match the specifics of my app.

1. Public disk URL changed from /assets to /storage

Shift defaulted my public disk URL to Laravel’s convention (/storage), but my app uses /assets. If you have a custom path and existing URLs in your database, this would silently break all file links. Reverted to /assets.

2. Bugsnag was a breaking dependency — but we don’t use it

Shift saw Bugsnag was installed and updated code to use Bugsnag::notifyException() directly. But I’d already migrated to Sentry, so this would have crashed. The idiomatic Laravel approach is report() — it routes to whatever error handler you’ve configured, so your code never needs to know whether that’s Bugsnag, Sentry, or just the log. Boost flagged this immediately because it could see the full codebase context. We removed Bugsnag entirely and kept report().

3. bkwld/cloner flagged as incompatible — removed entirely

Shift flagged bkwld/cloner as an incompatible dependency it couldn’t update for Laravel 13. Rather than waiting for an upstream fix, I removed the package entirely and replaced the Cloneable trait with a simple duplicate() method using Laravel’s built-in replicate():

public function duplicate(): self
{
    $clone = $this->replicate(except: [
        'plan', 'date_test_indesign', 'date_test_complete',
        'date_test_inprogress', 'date_test_analysing',
        'test_result', 'experiment_rating', 'starred',
        'is_template', 'experiment_completion_target', 'stage',
    ]);

    $clone->stage = IdeaStage::STAGE_IN_DESIGN;
    $clone->date_test_indesign = now();
    $clone->save();

    return $clone;
}

Eight lines, no external dependency, does exactly what I need. No need for a package when the framework already handles it.

4. #[Signature] attribute issues on console commands

Shift converted console commands from the old $signature property to Laravel 13’s #[Signature] PHP attribute. Two issues came up. First, one command was missing the use imports — without them, PHP treats #[Signature(...)] as a comment, so the command has no name:

// These were missing — caused "cannot have an empty name" error
use Illuminate\Console\Attributes\Description;
use Illuminate\Console\Attributes\Signature;

Second, Shift formatted some signatures across multiple lines. Laravel 13’s attribute parser can’t handle that:

// Broken — multiline
#[Signature('ideas:score
    {--id= : Score a specific idea by ID}
    {--all : Score all ideas}')]

// Fixed — single line
#[Signature('ideas:score {--id= : Score a specific idea by ID} {--all : Score all ideas}')]

Four commands had one or both of these issues.

Dependency blockers

Shift flagged 8 packages it couldn’t update. Boost investigated each one — checking Packagist, GitHub PRs, and the actual version constraints:

PackageL13 Support?Resolution
laravel/pulse✅ ReadyNo action needed
livewire/flux-pro✅ ReadyNo action needed
pestphp/pest-plugin-drift✅ ReadyNo Laravel constraint
bentonow/bento-laravel-sdk✅ ReadyNo Laravel constraint
algolia/scout-extended❌ PR openRemoved — migrating to Postgres search
bkwld/cloner❌ PR openRemoved — using replicate()
bugsnag/bugsnag-laravel❌ PR openAlready removed
prism-php/relay❌ PR openBoost submitted PR #36, using fork

For prism-php/relay, Boost forked the repo, made the one-line change (adding |^13.0 to the constraint), and opened the PR — modelled on the identical change Taylor Otwell made for the core Prism package.

prism-php/relay isn’t a blocker then right? i can still upgrade?

Right — using the fork meant we could proceed without waiting for the upstream merge.

Removing Algolia mid-upgrade

I was planning to migrate from Algolia to Postgres search anyway, so I removed algolia/scout-extended during the upgrade. Boost traced every Algolia reference across the codebase:

The fix was to keep laravel/scout with the database driver (SCOUT_DRIVER=database) and simplify the Livewire search to query Idea directly through Scout:

$this->results = Idea::search($this->search)
    ->where('team_id', $this->teamId)
    ->take(5)
    ->get()
    ->map(function ($idea) {
        $idea->highlightedName = preg_replace(
            '/(' . preg_quote($this->search, '/') . ')/i',
            '<strong>$1</strong>',
            $idea->name
        );
        $idea->tag = $idea->isExperiment() ? 'Experiment' : 'Idea';
        return $idea;
    })
    ->all();

Not as fuzzy as Algolia, but it works for now until the Postgres migration.

What didn’t matter

The upgrade guide lists 22 breaking changes. For my app, only 3 actually mattered:

  1. Cache/session prefix naming (environment variables)
  2. $casts property to casts() method (Shift handled this)
  3. PHP attribute adoption for console commands (Shift handled this, mostly)

Everything else — JobAttempted event changes, QueueBusy property renames, Container::call nullable defaults, MySQL DELETE query changes, pagination view names, Js::from unicode escaping — I wasn’t using any of them. Boost confirmed this by searching for each pattern.

The best of both worlds

If I’d used only Shift, I’d have merged a PR with a wrong disk URL, broken Bugsnag references, incompatible dependencies, and missing attribute imports. Shift doesn’t know my app — it applies patterns.

If I’d used only Boost, I’d have had to manually update every config file, convert every $casts property, adopt anonymous migrations, and modernise PHP syntax across dozens of files. That’s tedious work an agent shouldn’t have to reason about.

Together, Shift handles the 80% that’s mechanical. Boost handles the 20% that needs context, judgement, and codebase-specific knowledge. The combination gave me a clean upgrade with zero post-deploy issues.

Deploy checklist

For anyone about to do this:

  1. Pin CACHE_PREFIX, REDIS_PREFIX, SESSION_COOKIE in production .env before deploying
  2. Run Shift ($19) — let it handle the mechanical changes
  3. Review Shift’s PR with Boost — walk through each comment with full codebase context
  4. Check your packages against Laravel 13 compatibility
  5. Run composer update and commit the lockfile
  6. Deploy, then run php artisan migrate and php artisan view:clear
  7. Cross-check against the Boost upgrade prompt to catch anything both tools missed

The official estimate of 10 minutes is accurate for the framework changes. The real time goes into dependency compatibility, reviewing automated changes, and cleaning up package references. Having both tools made that significantly faster.


Back to top ↑