# Laravel upgrade plan: 11 → 12, then 13 (combined) **Combined by Claude Code, 2026-10-15** from four documents in this folder: - Claude's plan: `LARAVEL_UPGRADE_PLAN_CLAUDE.md` - Vibe's plan: `LARAVEL_UPGRADE_PLAN_VIBE.md` - Vibe's hybrid: `LARAVEL_UPGRADE_PLAN_HYBRID.md` - Claude's first hybrid, merged into this file **Status: proposal, waiting for the user's decisions** (section 11). Nothing is approved or started. Origin of each part: **[C]** Claude, **[V]** Vibe (its plan or its hybrid), **[C+V]** combined. Where a plan assumed something that does not match this codebase, the corrected fact is used (section 2). Section 10 lists what was left out and why; section 12 lists how the two hybrids differed. --- ## 1. Approach in short 1. **[C] Laravel 12 first, Laravel 13 as a second project right after it.** Laravel 12 keeps WireChat 0.2; Laravel 13 needs WireChat 0.6, the largest single risk. 2. **[C] Prepare on Laravel 11 with small, normal deploys** (Carbon 3, flatpickr into the app, remove unused packages), so the framework upgrade itself stays small. 3. **[V] Verify by area of this application**: multi-guard profiles, the two permission layers, white-label platforms, payments, chat, search, themes, background processes. Use a test matrix, a reusable verification command and explicit success criteria. 4. **[V] Measure before and after** (page times, search time, queue lag), and **decide on rollback with triggers set in advance**. 5. **[C+V] Reuse the proven tooling** (`scripts/upgrade.sh`: backup, snapshot, rollback, maintenance with a bypass) **with a deliberate order**: local rehearsals, then a test server if there is one, then production in a quiet window, then other instances. 6. **[V] Tell users in advance**, and keep a checklist for before, during and after. --- ## 2. Facts that both plans rest on Checked 2026-10-15 against Composer, Packagist and the code of branch `laravel-11`. | Topic | Fact | Note | |---|---|---| | Laravel releases | 12.70.0 and 13.36.0 are out | Vibe's plan said "when Laravel 12 is released" | | PHP | Laravel 12 needs PHP 8.2+, Laravel 13 needs **8.3+**; production runs 8.3.35 | Vibe's plan required 8.3 for 12; only 13 needs it | | Support | 11: no security fixes since March 2026 · 12: security fixes until 2027-02-24 | Verify on laravel.com/docs/master/releases | | Livewire | 3.8 has releases for Laravel 12 and 13: **4.0 is not needed** | Vibe's plan had a Livewire 4 migration | | Spatie Permission | we run **6.25**, which supports 12 and 13 | Vibe's plan had a v5 → v6 migration | | Reverb | 1.12 supports 12 and 13; **there is no Reverb 2** | | | Scout | 10.x supports 12; 11 is optional | | | Blockers | **flatpickr** (no release after Laravel 11), **Carbon 3** (28 `diffIn*()` calls), **`psr/simple-cache` pin**, **sluggable/taggable** new majors, unused **Mary** and **laradumps** | Not in Vibe's plan | | Laravel 13 only | **WireChat 0.6** (we have 0.2 with 45 override and view files), tinker 3, debugbar 4 | | | Backup and rollback | `scripts/upgrade.sh` already does verified backups, a vendor/build snapshot, a code and/or database rollback, and maintenance with a bypass; tested in 3 rehearsals and the 2026-10-15 production upgrade | Vibe's plan wrote new backup and rollback scripts: not needed, reuse | Package details: `references/plans/LARAVEL_UPGRADE_PLAN_CLAUDE.md`, section 3. --- ## 3. Phases ### Phase 0: Preparation, no code changes [C+V] 1. **[C]** Finish the Laravel 11 follow-ups: quiet logs, `main` fast-forwarded, snapshot removed. 2. **[V]** **Inventory per server**: PHP version, Composer version, free disk, services (workers, Reverb, scheduler) and the platform config name. Use one read-only block per server. Laravel 13 needs PHP 8.3 everywhere. 3. **[V]** **Baseline measurements on production** (for section 5), taken before anything changes. 4. **[C]** **Decision**: 12 then 13 (recommended), or straight to 13. 5. **[V]** **Is there a test or staging server?** If not, decide whether to set one up or rely on local rehearsals (which worked for Laravel 11). ### Phase 1: Preparation on Laravel 11 (normal deploys, one step at a time) [C] Each step is a commit on `main`, the full suite passes, and it ships with the normal `deploy.sh`. 1. **Carbon 3.** - `nesbot/carbon ^3.8`. - Review the 28 `diffIn*()` calls: in Carbon 3 they are signed and fractional, which would give, for example, negative durations in the inactivity mails. - Add tests for durations shown to people. 2. **Composer clean-up.** - Drop the `psr/simple-cache ^1.0` pin. - Remove `robsontenorio/mary` (with `config/mary.php`) and `laradumps/laradumps`. 3. **Flatpickr into the app.** - The component (already overridden in `app/Overrides/Asdh`), its views and its assets move into the app under the same name, so the 21 `` views stay unchanged. - Remove the package. 4. **Updates within the current lines**: Collision 8.6+, the `staudenmeir` packages, Livewire 3.8.x. 5. **[V] After each deploy**: the verification command (section 4.2) and a short look at the logs. **[V] Commands** (each step is its own commit; run the full suite after each): ```bash composer require nesbot/carbon:^3.8 -W # 1, plus the diffIn*() review composer remove robsontenorio/mary laradumps/laradumps # 2, plus deleting config/mary.php composer require psr/simple-cache:^3.0 -W # 2, or remove the pin from composer.json composer remove asdh/laravel-flatpickr # 3, after the component lives in the app composer update nunomaduro/collision "staudenmeir/*" livewire/livewire -W # 4 ``` (`cviebrock/eloquent-sluggable` and `-taggable` cannot move on Laravel 11: their next majors need Laravel 12, so they belong to Phase 2. The flatpickr component goes to `app/View/Components/`, the Laravel convention.) **[V] Phase 1 is complete when:** - [ ] Carbon 3, the clean-up and flatpickr are on production through normal deploys - [ ] the full suite and `upgrade:verify` pass - [ ] three days without new error types in the production log ### Phase 2: Laravel 12 (branch `laravel-12`) [C+V] 1. **[C] Composer:** - `laravel/framework ^12.0`, `cviebrock/eloquent-sluggable ^12.0`, `cviebrock/eloquent-taggable ^12.0`, then `composer update -W`. - Write down every major version that changed in the lock file. 2. **[C] Upgrade guide checked against the code**: Carbon applies. SVG images, the `local` disk root, UUIDv7, `mergeIfMissing`, `Concurrency`, `Schema::getTables()` and the token repository are not used or not affected. Diff the skeleton config 11 → 12. 3. **[C] Overrides**: diff the vendor originals old → new for every override and published view (Jetstream `HasProfilePhoto`, Matchish, WireChat views, WireUI) and port the changes. 4. **[C+V] Tests:** - full suite - the verification command (4.2) - the manual test matrix (4.3), with all 4 themes 5. **[C] Rehearsals with `upgrade.sh`:** - a clone of `main` with a copy of the database (success path, rollback before and after going live) - the fictitious Uuro platform - a routine deploy afterwards 6. **[V] Test server** (if decided in Phase 0): a real run there, the matrix, and fixes on `laravel-12`. 7. **[C] Docs**: `references/UPGRADE_LARAVEL_11_TO_12.md`, versions in `AGENTS.md`. 8. **[C+V] Rollout**: see section 6, per instance with `--target origin/laravel-12`; fast-forward `main` when all are done. **[V] Commands:** ```bash git switch -c laravel-12 main # only after the user agrees: a whole-tree git command composer require laravel/framework:^12.0 cviebrock/eloquent-sluggable:^12.0 cviebrock/eloquent-taggable:^12.0 -W git diff composer.lock | grep -E '"version"' | head # write down every major that moved ``` **[V] Phase 2 is complete when:** - [ ] Laravel 12 runs on every instance - [ ] `upgrade:verify` and the high-priority matrix rows pass on each - [ ] three days without new error types per instance ### Phase 3: Laravel 13 (separate project, right after Laravel 12 is live) [C] 1. PHP 8.3 on every instance (from the Phase 0 inventory). 2. **WireChat 0.2 → 0.6**: - read the changelogs - re-port the 45 override and view files, the `_finishUpload` override, private attachments and the listeners across profile switches - extend the chat tests first - can be prepared on Laravel 12 (WireChat 0.6 supports `^11|^12|^13`) 3. tinker 3, debugbar 4, sluggable/taggable 13; Symfony 7.4/8, Monolog 3.10. 4. The official 12 → 13 upgrade guide, checked against the code item by item. 5. Same method as Phase 2. **Finish before 2027-02-24.** **[V] Commands:** ```bash composer require laravel/framework:^13.0 namu/wirechat:^0.6 laravel/tinker:^3.0 \ cviebrock/eloquent-sluggable:^13.0 cviebrock/eloquent-taggable:^13.0 -W composer require --dev barryvdh/laravel-debugbar:^4.0 -W ``` **[V] Phase 3 is complete when:** - [ ] Laravel 13 runs on every instance, PHP 8.3+ everywhere - [ ] chat works as before: messages, images, private attachments, profile switches - [ ] `upgrade:verify`, the full suite and the matrix pass - [ ] three days without new error types per instance --- ## 4. Verification ### 4.1 Automated **[C]** The full suite: 656 tests, green on Laravel 11. It runs on every phase step and before every rollout. ### 4.2 Verification command [V idea, C implementation] **[V]** A read-only check per area, runnable locally and on every server after each step. Instead of three tinker scripts, **[C]** one artisan command, `php artisan upgrade:verify`, with correct checks and a non-zero exit code on failure: | Area | Check | |---|---| | Framework | Laravel and PHP version, `config:merge --all --dry-run` clean | | Profiles and guards | the 4 guards configured; one User, Organization, Bank and Admin load; the `getActiveProfile()` helpers exist | | Permissions | for every `AdminRole` and `OrganizationRole`: each scoped Spatie role with that suffix has **exactly** the same permission set (sorted arrays compared, not the return value of `sort()`); missing scoped roles listed | | White-label | `config('app.timebank_config')` set and its file exists; `platform_name()` is not empty and not `Example Timebank`; `theme_color('brand')` set; `local_pages` and `android_app` present | | Payments | the app's MySQL user may not UPDATE or DELETE `transactions` (reuse `scripts/test-transaction-immutability.sh`) | | Search | a Scout search returns results for each searchable model | | Background | queue sizes per queue; Reverb answers on its port | Written in Phase 1 (on Laravel 11), so the baseline is known before the framework changes. Done 2026-10-15 (branch `prep-l12-verify`): `php artisan upgrade:verify [--only=area,...]`. Two checks turned out to need care: `organization_roles` is created by `database:update`, not by a migration (a warning when missing), and roles without a scope prefix are base roles and are skipped. ### 4.3 Manual test matrix [V, trimmed to this application] | Area | Cases | Priority | |---|---|---| | Profiles | register, log in and out, switch User → Organization → Bank → Admin and back, session survives the switch | high | | Permissions | admin pages per role, organization and bank manager rights, edit a role and see it take effect | high | | Payments | a payment, balance and history updated, a call paused at zero credits | high | | Chat | message between profiles, image upload (6 MB), message after a profile switch | high | | Search | results in en and nl, a category browse | high | | White-label | all 4 themes; a platform with its own config (the Uuro rehearsal) | high | | Mail | a mailing preview, a test mail per type (`email:send-test`), inactivity mail texts (Carbon) | medium | | PDF | an account statement, a post PDF | medium | | Dates | every date picker (flatpickr) | medium | | Background | queue workers process jobs, Reverb presence, scheduler tasks | medium | --- ## 5. Measurements and success criteria [V, adapted] **Baseline (Phase 0) and after each rollout** (simple, production-safe, no load test on production): | Metric | How | |---|---| | Response time of the home page, a profile page, search, the payment page | `curl -o /dev/null -s -w '%{time_total}'`, 5 runs each, median | | Search time | timed Scout query in tinker | | Queue lag | `Queue::size()` per queue, and the age of the oldest job | | Errors | `.ERROR` count per day in `laravel-.log` | **Done when:** - the full suite and `upgrade:verify` pass; - the manual matrix's high-priority rows pass; - no new error types in the log for 3 days; - response times are within 25% of the baseline. --- ## 6. Rollout and rollback [C+V] - **Order [V]:** local rehearsals → test server (if any) → production → other instances, one at a time. - **Window [V, adapted]:** - production at a quiet moment the admin is awake for (for example an evening, 20:00–22:00 local time), not 02:00 - 5 to 15 minutes of maintenance, by the measure of the Laravel 11 upgrade - **Tool [C]:** `scripts/upgrade.sh` from the target branch, run in tmux, with a dry run first. - **Rollback triggers, decided in advance [V]:** - **before going live**, roll back (the database restore is allowed) when: - `deploy.sh` fails and the cause is not fixed within 30 minutes - any high-priority row of the matrix fails - `upgrade:verify` fails - **after going live**, roll back the code only when: - logging in, payments or chat break and a fix is not available within an hour - errors repeat for many users - **Code-only by default after going live [C]:** a database restore deletes payments and messages written since the backup. --- ## 7. Communication [V, sized for this platform] - **24 hours before**: a short message to users, a post or a banner: date, time, expected few minutes of downtime. - **During**: the maintenance page; the admin tests through the bypass URL. - **After**: a short "done" note only if something visible changed. - **Between the agents [C]**: `.agents/WORKLOG.md` entries per phase step; one agent per step; the user dispatches. --- ## 8. Checklists [V, corrected] **Before (per instance):** - [ ] Phase 0 inventory done; PHP and disk sufficient - [ ] `upgrade:verify` passes on the current version - [ ] baseline measured - [ ] users informed - [ ] the MySQL root password at hand (no `DB_DEPLOY_*` in `.env`) **During:** - [ ] tmux, dry run, real run - [ ] maintenance on - [ ] config merge `y`/`yes` - [ ] restart Reverb, PHP-FPM and workers - [ ] verification and matrix through the bypass - [ ] go or roll back by the triggers **After:** - [ ] `upgrade:verify` and measurements - [ ] workers `active` - [ ] logs for 3 days - [ ] fast-forward `main` when all instances are done - [ ] remove the snapshot - [ ] lessons learned added to the upgrade guide --- ## 9. Effort (working time, rough) [C, with Vibe's verification work added] | Phase | Effort | |---|---| | 0 | 1 to 2 hours (inventory, baseline, decisions) | | 1 | 2 to 3 days (Carbon review, flatpickr, `upgrade:verify`), plus normal deploys | | 2 | 2 to 3 days, including rehearsals, plus a rollout evening per instance | | 3 | 3 to 5 days, mainly WireChat 0.6 | --- ## 9a. Timeline [V idea, C dates] Vibe's hybrid put Laravel 13 live on 2026-10-26. That is too early: Phase 0 depends on the Laravel 11 follow-ups (quiet logs until 2026-10-18), and each production change needs a few quiet days before the next one. A realistic schedule, to be confirmed by the user: | Phase | Work | Calendar window | Milestone | |---|---|---|---| | 0 | 1 to 2 hours | from 2026-10-19 | inventory, baseline, decisions | | 1 | 2 to 3 days | late October to early November | Carbon 3, flatpickr, clean-up on production | | 2 | 2 to 3 days | November | Laravel 12 on every instance | | 3 | 3 to 5 days | December to January | Laravel 13 on every instance | | deadline | | **before 2027-02-24** | end of Laravel 12 security fixes | --- ## 10. What was left out, and why | From | Left out | Why | |---|---|---| | Vibe | Livewire 4, Spatie v5 → v6, Reverb 2, Scout 11 migrations | not needed (section 2) | | Vibe | new backup and rollback scripts | `upgrade.sh` already does it, tested | | Vibe | `composer.json` PHP requirement to `^8.3` for Laravel 12 | only Laravel 13 needs it; do it in Phase 3 | | Vibe | load test with 100 concurrent users on production | puts load on the live site; response-time samples are enough here | | Vibe | a 02:00 production window and a status page | the admin must be awake to test and decide; the platform is small | | Vibe's hybrid | running Vibe's three original verification scripts | they read a non-existent config key (`platform_name`) and compare `sort()` return values, so permissions always look in sync; replaced by `upgrade:verify` (4.2) | | Vibe's hybrid | decisions marked as made, "Approved Strategy" | the user has not decided yet (section 11) | | Vibe's hybrid | production at 02:00–04:00, Laravel 13 by 2026-10-26 | see section 6 and the timeline in 9a | | Claude | — | everything kept | --- ## 11. Open questions for the user 1. Which instances exist besides timebank.cc, and on which PHP version? 2. Is there a test or staging server, or should one be set up? 3. 12 then 13 (recommended), or straight to 13? 4. A preferred rollout window for production? 5. Should `upgrade:verify` become a permanent command, also useful after normal deploys? --- ## 12. How the two hybrid plans differed Both hybrids, written at the same time without seeing each other, chose the same structure: Claude's phases (preparation on 11, then 12, then 13) with Vibe's verification and test matrix. The differences, and what this plan uses: | Topic | Vibe's hybrid | Claude's hybrid | This plan | |---|---|---|---| | Verification | Vibe's 3 tinker scripts | one `upgrade:verify` command | `upgrade:verify` (correct checks) | | Measurements, rollback triggers, communication | not included | included (from Vibe's own plan) | included | | Completion criteria per phase | yes | overall only | per phase (from Vibe's hybrid) | | Commands per phase | yes | no | yes, corrected | | Timeline | calendar dates, Laravel 13 by 2026-10-26 | effort only | effort plus realistic windows | | Decisions | marked as decided | open | open | | Production window | 02:00–04:00 | an evening the admin is awake | an evening |