Upgrading Dependencies
Dependency upgrades are part of normal maintenance, but not all upgrades should be treated the same way.
- Patch and minor updates are usually routine.
- Major updates are deliberate engineering work.
- The important distinction is not only "how do we install updates?" but also "how do we notice versions that our current constraints intentionally do not allow?"
This project uses two ecosystems:
- Backend: Composer in
backend/composer.json - Frontend:
vpas the primary toolchain interface, backed by Bun-managed dependencies infrontend/package.json, with a root-levelvite-plushelper pin inpackage.jsonfor workspace commands such asvp run ...andvp config
For the frontend, vp is the command surface we use day to day:
vpowns the dev/build/test/lint/package-management command surface (vp dev,vp build,vp check,vp test,vp install,vp update,vp outdated).- Bun still owns the underlying package resolution and lockfile format through
vp.
Most direct dependencies use SemVer ranges such as ^13 or ^5.2. That is good for stability, but it also means composer update and frontend package-manager updates will usually stop at the newest version inside the current major line. They do not automatically move us to a new major version.
Upgrade Tiers
| Type | Example | Usual handling |
|---|---|---|
| Patch | 1.2.3 -> 1.2.4 | Routine update |
| Minor | 1.2 -> 1.3 | Routine update with full verification |
| Major | 1.x -> 2.0 | Planned upgrade branch |
Even patch and minor updates can break in practice, so every upgrade should be followed by the relevant tests and checks.
Execution Modes
There are two useful ways to run this protocol:
- Rehearsal mode: non-mutating checks that prove the process works before touching lockfiles.
- Real update mode: actually change dependencies, lockfiles, and any generated artifacts triggered by package-manager hooks.
Use rehearsal mode first when you want a safe signal about whether the current branch is healthy enough for upgrade work.
Rehearsal Mode
cd frontend && vp update -- --dry-run
cd backend && composer update --dry-runThen run the same verification suite you would use after a real update.
Routine Updates
Frontend
These commands are mutating. In this repo, vp update updates the installed dependency graph, refreshes frontend/bun.lock, and may also rewrite version ranges in frontend/package.json to the newer resolved releases.
Run from the repository root or from frontend/:
cd frontend
vp updateReview both frontend/package.json and frontend/bun.lock after the update instead of assuming the manifest stayed unchanged.
If the goal is specifically to refresh the frontend toolchain itself, check the Vite+ packages too:
cd frontend
vp update vite-plus vite vitestIf you intentionally change the Vite+ toolchain version, also sync the root package.json helper pin (vite-plus plus the root vite and vitest overrides) so workspace-level vp run ... and vp config stay aligned with the frontend.
Then verify:
vp check
vp testBackend
These commands are mutating. They may also trigger Composer post-update hooks that regenerate published assets.
Run from backend/:
cd backend
composer updateIn this repo, composer update runs Laravel and Filament post-update hooks. Even when the lockfile does not change, those hooks can republish generated assets under backend/public/. Review those diffs separately from actual dependency changes.
Then verify:
php artisan test --parallel
composer phpstan
composer deptracIf php artisan test --parallel fails locally with PostgreSQL errors like out of shared memory or max_locks_per_transaction, treat that as a local database-capacity issue first, not automatic evidence of an upgrade regression. For upgrade debugging, either raise the Postgres limit or rerun the backend tests without parallelism to separate environment pressure from application breakage.
How To Detect New Major Versions
This is the part people often miss.
When a dependency is constrained with ^, the package manager will usually not upgrade across a major boundary during a normal update. So we need a separate "major scan".
Composer
Composer has first-class support for this:
cd backend
composer outdated --direct --major-onlyThis shows direct dependencies where a newer major exists.
Useful variants:
composer outdated --direct
composer outdated --direct --format=jsonInterpretation:
composer updateanswers: "What can we safely install within current constraints?"composer outdated --major-onlyanswers: "What new major lines exist beyond our current constraints?"
Composer Transitive Dependencies
Sometimes composer outdated surfaces a package that we do not require directly. Treat that as a dependency-graph question, not an automatic upgrade task.
Recommended check order:
cd backend
composer audit
composer why brick/math
composer prohibits brick/math 0.17.1Interpretation:
- If
composer auditis clean, there is usually no urgency. composer whyshows which direct or transitive packages pull the dependency into the graph.composer prohibitsshows which package is blocking a specific target version.
Do not add a transitive package to composer.json just to force a newer version unless we have a concrete reason, such as a security advisory, a bug we are actually hitting, or a direct feature/API need.
Example: brick/math 0.14.8 may appear outdated while still being the correct resolved version. In this repo, Laravel allows newer brick/math, but ramsey/uuid 4.9.2 still limits it to ^0.14, so trying to force brick/math 0.17.1 creates solver friction without practical benefit.
Frontend (vp)
vp does not have a dedicated --major-only flag like Composer, but vp outdated gives the information we need:
cd frontend
vp outdatedThe output includes:
Current: installed versionUpdate: newest version allowed by the current rangeLatest: newest version published
After the Vite+ migration, pay particular attention to the toolchain packages in frontend/package.json:
vite-plusvite(aliased to@voidzero-dev/vite-plus-core)vitest(aliased to@voidzero-dev/vite-plus-test)
And remember that the repository root also carries a vite-plus helper pin plus vite/vitest overrides in package.json. That root file does not drive the SPA build itself, but it does affect root-level vp workflows.
Interpretation:
- If
UpdateandLatestare the same, your current range can already reach the latest release. - If
Updateis behindLatest, a newer version exists outside your current allowed range. - In practice, that often means a new major is available and your
^range is intentionally blocking it.
Example:
| Package | Current | Update | Latest |
| lucide-react | 0.562.0 | 0.562.0 | 0.577.0 |That means a newer release exists, but vp update will not take it with the current constraint.
Recommended Cadence
Use two different rhythms:
1. Routine dependency maintenance
Do this regularly:
cd frontend && vp updatecd backend && composer update- Run the normal verification suite
2. Major-version scan
Do this on a schedule, for example once a month:
cd frontend && vp outdated
cd backend && composer outdated --direct --major-onlyThis keeps major upgrades visible without forcing them into every routine maintenance pass.
Major Upgrade Process
When we decide to take a major version, treat it as a small project.
Principles
- Upgrade one major dependency at a time when feasible.
- Do not combine unrelated major upgrades in one branch unless there is a strong reason.
- Read the official upgrade guide before changing code.
- Start from a clean baseline with passing tests and analysis.
- Prefer small, reviewable commits over one giant "upgrade everything" diff.
Workflow
1. Read the upstream guide
Examples:
- Laravel: the target major's upgrade guide, for example https://laravel.com/docs/13.x/upgrade when moving onto Laravel 13
- Filament: the target major's upgrade guide, for example https://filamentphp.com/docs/5.x/upgrade-guide when moving onto Filament 5
- React ecosystem packages: release notes / migration docs for the specific package
2. Establish a clean baseline
Do the safe rehearsal first so you know whether the branch is already unstable before you mutate anything.
# Backend
cd backend
php artisan test --parallel
composer phpstan
composer deptrac
# Frontend
cd ../frontend
vp update -- --dry-run
vp check
vp testIf the baseline is already broken, fix that first. Upgrade work is much harder to reason about on top of existing failures.
3. Create a dedicated branch
Example:
git checkout -b upgrade/filament-64. Upgrade the dependency explicitly
Examples:
# Composer
cd backend
composer require filament/filament:^6.0 --update-with-dependencies
# Bun
cd ../frontend
vp add some-package@1.2.3For frontend packages, prefer an explicit target version instead of blindly taking latest, especially when the toolchain is still evolving quickly.
For Vite+ toolchain upgrades, prefer explicit package names so the aliasing stays obvious in review:
cd frontend
TARGET_VP=0.1.23
vp add -d vite-plus@"$TARGET_VP" vite@npm:@voidzero-dev/vite-plus-core@"$TARGET_VP" vitest@npm:@voidzero-dev/vite-plus-test@"$TARGET_VP"Then mirror the same target version in the root package.json helper pin and overrides before reviewing the combined lockfile diff.
After the real update command finishes, inspect lockfile and generated-asset diffs before assuming every changed file represents a real behavioral change.
5. Fix breakages iteratively
Suggested order:
- Static analysis and type checks
- Automated tests
- Manual smoke testing of the affected flows
6. Regenerate artifacts when relevant
If backend API signatures changed:
cd frontend
vp run api:generate7. Document what changed
If the upgrade taught us project-specific lessons, add them to this document so the next upgrade starts from real local knowledge, not memory.
What Not To Assume
- A normal
updatecommand does not mean "we are fully up to date". - A green lockfile refresh does not mean "no major upgrades exist".
- SemVer helps, but it does not remove the need for tests.
- Some ecosystems, especially frontend tooling, occasionally ship breaking behavior in minor releases. Verify, do not trust blindly.
Version History
Routine Composer and frontend refresh (July 2026)
This was a routine in-range dependency refresh, not a major-version upgrade.
Local lessons:
- Newer Larastan reports
env()calls outsideconfig/withlarastan.noEnvCallsOutsideOfConfig; move those values into config keys and read them withconfig()from providers, helpers, services, and controllers. - Newer Vite+/Vitest mock handling is stricter about missing mocked exports and hoisting. If a module imports more of a mocked package, update the mock to expose those exports, and use
vi.hoisted()for state referenced by a hoistedvi.mock()factory. - Full frontend test runs may time out under heavy parallel load even when the affected files pass directly. Rerun the listed failing files before assuming the upgrade introduced deterministic regressions.
PHP 8.2 -> 8.5 and Filament 3.1 -> 5.0 (February 2026)
This was completed together, which worked, but is not the preferred pattern for future upgrades.
Main breakage areas:
- Filament resource and page APIs changed significantly between v3 and v5:
Form->Schema, actions moved namespaces, and layout/schema components shifted SpatieLaravelTranslatablePluginwas replaced byLaraZeus\\SpatieTranslatable\\SpatieTranslatablePluginfor Filament v5 compatibilityfilament-shield,filament-users, andfilament-impersonateall required major-version alignment with Filament itself- PHPUnit major upgrades tightened some mocking behavior and required test updates; the first sharp edge in this repo showed up during 11 -> 12, and the current line is now
^13.1 - PHP 8.5 itself was relatively smooth thanks to existing static analysis coverage
Current Versions
| Dependency | Version |
|---|---|
| PHP | ^8.5 |
| Laravel | ^13.0 |
| Filament | ^5.2 |
| PHPUnit | ^13.1 |
| React | ^19.2 |
| Vite+ (frontend toolchain) | 0.1.23 |
| Vite+ (root helper pin) | 0.1.23 |
| TypeScript | ~6.0 |