Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

+360
View File
@@ -0,0 +1,360 @@
# 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 `<x-flatpickr>` 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-<date>.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 |
@@ -0,0 +1,244 @@
# Claude's plan: the next Laravel upgrade (11 → 12, then 13)
**Author: Claude Code** · Written 2026-10-15 · Status: proposal, not started
Written to compare with Mistral Vibe's plan (`LARAVEL_UPGRADE_PLAN_VIBE.md`). Every fact about this codebase
below was checked on 2026-10-15 against branch `laravel-11` (8e01ebc4) and Packagist. How: see
[Appendix](#appendix-how-the-facts-were-checked).
---
## 1. Recommendation in short
1. **Go to Laravel 12 first, and plan Laravel 13 as a second project right after it.** Laravel 12 can keep
our heavily customised WireChat 0.2. Laravel 13 needs WireChat 0.6, the largest single risk in either
upgrade. Doing them separately keeps each step small and each rollback simple.
2. **Do the risky preparation on Laravel 11 first**, as normal small deploys: Carbon 3, removing two unused
packages, and taking the abandoned flatpickr package into our own code. The Laravel 12 upgrade itself is
then a small change.
3. **Reuse what worked for Laravel 11**: `scripts/upgrade.sh`, a local rehearsal on a copy of the database, a
rehearsal on a fictitious second platform, and a staged rollout from a separate branch.
4. **Timing matters.** Laravel 11 has had no security fixes since March 2026. Laravel 12 gets security fixes
only until 2027-02-24, so Laravel 13 has to follow soon after.
---
## 2. Where we stand
| | |
|---|---|
| Production | Laravel 11.57.0 since 2026-10-15, PHP 8.3.35, Livewire 3.8 |
| Other instances | to be listed (PHP version per server matters for Laravel 13) |
| Newest Laravel 12 | 12.70.0, needs PHP 8.2+, Carbon 3 |
| Newest Laravel 13 | 13.36.0, needs **PHP 8.3+**, Carbon 3, Symfony 7.4/8 |
| Support (verify on laravel.com/docs/master/releases) | 11: security fixes ended March 2026 · 12: security fixes until 2027-02-24 · 13: about a year longer than 12 |
---
## 3. What blocks the upgrade, in this codebase
### 3.1 Packages
Most packages already have a release for Laravel 12 and 13 **in the major version we use now** (Livewire 3,
Jetstream 5, Fortify, Sanctum, Scout 10, spatie permission 6, medialibrary 11, Reverb, WireUI 2,
laravel-localization 2, dompdf, excel 3, and others). The exceptions:
| Package | Use here | Laravel 12 | Laravel 13 | What to do |
|---|---|---|---|---|
| `asdh/laravel-flatpickr` | `<x-flatpickr>` in 21 views; we already override its component class | **no release** (stops at Laravel 11) | **no release** | Move it into the app (one class, 2 views, CSS/JS), drop the package |
| `nesbot/carbon` | pinned at `^2.72` | needs **Carbon 3** | Carbon 3 | Upgrade on Laravel 11 first (Laravel 11 supports Carbon 3) |
| `psr/simple-cache` | pinned at `^1.0` in our composer.json (left over from the skeleton) | Laravel 12.29+ needs `^3.0` | `^3.0` | Remove the pin or set `^3.0` |
| `robsontenorio/mary` | **not used** (only `config/mary.php`) | needs 2.x | needs 2.x | Remove the package and its config |
| `laradumps/laradumps` (dev) | **no `ds()` calls** | needs 5.x | needs 5.x | Remove |
| `cviebrock/eloquent-sluggable`, `-taggable` | 24 files | 12.x | 13.x | Version bump (these majors only follow Laravel) |
| `nunomaduro/collision` (dev) | test runner | 8.6+ (we have 8.5) | 8.x | Update within 8.x |
| `staudenmeir/*` (3 packages) | relations | newer minor | newer minor | Update within the current line |
| `namu/wirechat` | chat; **45 override and published view files** | 0.2.11 works | **0.6 required** | Laravel 13 only: a project of its own |
| `laravel/tinker`, `barryvdh/laravel-debugbar` (dev) | | current line works | new major | Laravel 13 only |
### 3.2 Carbon 3: the main code risk
Carbon 3 changed `diffIn*()`: the results are now **signed** (negative when the argument is in the past) and
**floats**. Laravel 11 accepts Carbon 3, so this can be fixed and deployed before Laravel 12.
- 28 `diffIn*()` calls in 20 files need review.
- Example that would break: `now()->diffInSeconds(Carbon::parse($lastLoginAt))` in
`InactiveProfileWarning2Mail` and `InactiveProfileWarningFinalMail` becomes negative.
- Calls that already pass `false` (signed) or cast to `(int)` need a check for the float, not the sign.
- 172 `Carbon::parse` and 45 `Carbon::now` calls: mostly unaffected, but run the full suite and look at
date displays (transaction lists, statements, inactivity mails, call expiry).
### 3.3 Laravel 12 upgrade-guide items checked against the code
| Change in Laravel 12 | Here |
|---|---|
| Carbon 3 required | yes, see 3.2 |
| `image` validation rule excludes SVG | not affected: the one upload rule uses `mimes:jpg,jpeg,png,svg` |
| `local` disk default root moves to `storage/app/private` | not affected: `config/filesystems.php` sets `root => storage_path('app')` |
| `HasUuids` generates UUIDv7 | not used |
| `mergeIfMissing` with nested arrays | not used |
| `Concurrency` result keys | not used |
| `Schema::getTables()` and similar, multi-schema | not used |
| `DatabaseTokenRepository` expiry in seconds | no custom repository |
| Route precedence for duplicate names, container default values, `Str::is` with newlines | low; covered by the test suite and smoke tests |
To do in Phase 2: read the full official guide once more, and diff the `laravel/laravel` skeleton 11 → 12
(config files) against ours.
### 3.4 Tests
- 466 test methods, 656 tests including data sets, green on Laravel 11.
- PHPUnit 10.5 can stay for Laravel 12.
- 20 test files still use `@test` docblock annotations. They work on PHPUnit 10 and 11, but PHPUnit 12
ignores them, so converting them to `#[Test]` attributes is preparation for later.
---
## 4. Phases
### Phase 0: Before starting (no code changes)
1. Finish the Laravel 11 follow-ups: a quiet prod log, `main` fast-forwarded to `laravel-11`, snapshot removed
(see the post-upgrade reminder in shared memory).
2. List every instance and its PHP version. Laravel 13 needs PHP 8.3 everywhere.
3. Decide: Laravel 12 then 13 (recommended), or straight to 13 (see [section 6](#6-alternatives-considered)).
### Phase 1: Preparation on Laravel 11 (small changes, normal deploys)
Each step is its own commit on `main`, tested with the full suite, and deployed with the normal
`deploy.sh`, so production gets smaller changes one at a time instead of one big jump.
1. **Carbon 3.**
- `nesbot/carbon` to `^3.8`.
- Review all 28 `diffIn*()` calls: keep the meaning ("how long ago" stays positive, whole numbers where
shown to people), with `abs()` and `(int)` or `floor()` where needed.
- Add tests for the inactivity mails and call expiry, which compute durations.
2. **Composer clean-up.**
- Remove the `psr/simple-cache` pin.
- Remove `robsontenorio/mary` (and `config/mary.php`) and `laradumps/laradumps`.
3. **Flatpickr into the app.**
- Move the component (our override in `app/Overrides/Asdh`), its two views and its CSS/JS into the app.
- Register it under the same name `flatpickr`, so the 21 views keep `<x-flatpickr>` unchanged.
- Remove the package, the override and its classmap entry.
- Test every date picker: transactions table, select period, profile form, posts, calls.
4. **Updates within the current lines.**
- Collision to 8.6+, the `staudenmeir` packages, and the newest Livewire 3.8.x.
- `composer update` within the constraints, then the full suite.
### Phase 2: Laravel 12 (branch `laravel-12`, staged rollout)
1. **Branch** `laravel-12` from `main` once Phase 1 is on `main`.
2. **Composer:**
- `laravel/framework ^12.0`, `cviebrock/eloquent-sluggable ^12.0`, `cviebrock/eloquent-taggable ^12.0`.
- `composer update -W`. Write down every major version that changed in the lock file.
3. **Upgrade guide and skeleton:** apply section 3.3, and diff the Laravel 11 and 12 skeleton config files
against ours.
4. **Our overrides.** For every package with an override or published views (Jetstream `HasProfilePhoto`,
Matchish, WireChat views, WireUI), diff the vendor original between the old and new version and port the
changes, as was done for WireChat 0.2.11.
5. **Tests:**
- Full suite green.
- Render the main Livewire pages.
- Smoke test by hand: log in, switch profile, payment, chat with an image, poll, search, mailing preview,
PDF statement, date pickers.
- All 4 themes.
6. **Rehearsals with `scripts/upgrade.sh`:**
- a clone of `main` with a copy of the database (success path, rollback before and after going live)
- the fictitious Uuro platform (`TIMEBANK_CONFIG=uuro`, own config, own theme)
- a routine deploy afterwards
The script is version-neutral. It reads the PHP requirement from the target lock file, but check its
texts ("Laravel 10 -> 11") and the version check in step 6.
7. **Docs:**
- `references/UPGRADE_LARAVEL_11_TO_12.md` (the 10 → 11 guide with the changes)
- versions in `AGENTS.md`
- the post-upgrade checklist
8. **Rollout:**
- the development server first, then production, then other instances: `--target origin/laravel-12`
- fast-forward `main` once all instances are upgraded or ready
### Phase 3: Laravel 13 (separate project, start right after Laravel 12 is live)
1. **PHP 8.3 on every instance** (production already has 8.3.35).
2. **WireChat 0.2 → 0.6**, the biggest item:
- Read the changelogs of 0.3 to 0.6.
- Re-port our 45 override and view files.
- Check our `_finishUpload` override (Livewire 3.8 signed uploads), private attachments and the listeners
that work across profile switches.
- Extend the chat tests first, so regressions show.
- This can be prepared on Laravel 12 if WireChat 0.6 also supports 12 (it does: `^11|^12|^13`).
3. **Package majors:** tinker 3, debugbar 4 (dev), sluggable and taggable 13; check Symfony 7.4/8,
Monolog 3.10 and Guzzle.
4. **Read the official 12 → 13 upgrade guide.** I do not list its items from memory here; they have to be
checked against the code, as in 3.3.
5. **Same method** as Phase 2: branch `laravel-13`, rehearsals, staged rollout.
6. **Finish before 2027-02-24**, when Laravel 12 gets its last security fixes.
---
## 5. Risks and how to limit them
| Risk | Impact | Limit it by |
|---|---|---|
| Carbon 3 changes durations silently (negative or fractional) | Wrong texts in mails ("-3 days ago"), wrong expiry decisions | Phase 1 on Laravel 11 with tests per `diffIn*()` call; check mails per theme |
| Flatpickr removal breaks date pickers | Forms where a date cannot be entered | Same tag name; test every page with a picker |
| A package's new major changes behaviour (sluggable, taggable) | Wrong slugs or tags | Read their changelogs; tests on posts, tags, calls |
| Override drift (vendor views and classes) | Chat or profile photo regressions | Diff vendor originals old to new, as for WireChat 0.2.11 |
| WireChat 0.6 (Laravel 13) | Chat broken | Separate phase, tests first, prepare on Laravel 12 |
| A server with older PHP | Laravel 13 cannot run there | List PHP versions in Phase 0; `upgrade.sh` refuses too old PHP |
| A long-running branch drifts from `main` | Rebase conflicts during the rollout | Short phases; do not rebase after a server upgraded from the branch |
| Two AI agents in one folder | Conflicting edits during the upgrade | Worklog entries; one agent per phase step; the user dispatches |
---
## 6. Alternatives considered
- **Straight to Laravel 13 in one project.** One rollout instead of two, and the longest support. But it
combines Carbon 3, all package majors, the Laravel 12 and 13 framework changes and WireChat 0.6 in one
step, so a failure is harder to pin down and a rollback undoes more. Reasonable only if Phase 1 is done
and WireChat 0.6 has been tried separately first, so in practice it is the same work in a different order.
- **Laravel 12 only, and stay there.** Its security support ends 2027-02-24. Not an end state.
- **Doing Phase 1 inside the Laravel 12 branch.** Possible, but then production gets Carbon 3, flatpickr
and the framework at once. Phase 1 on Laravel 11 gives smaller, easier-to-check production changes.
---
## 7. Effort (working time, rough)
| Phase | Effort |
|---|---|
| 0 | an hour (an inventory and a decision) |
| 1 | 1 to 2 days (Carbon review is most of it), 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, plus rehearsals and rollout |
---
## 8. Open questions for the user
1. Which instances exist besides timebank.cc, and on which PHP version?
2. Is there still a development server to upgrade first?
3. Agree with Laravel 12 first and 13 straight after, or prefer straight to 13?
4. Is Mary perhaps planned for future UI work? (It is not used now.)
---
## Appendix: how the facts were checked
- **Package support:**
- The Packagist metadata of every package in `composer.json` was read with
`.agents/archive/laravel-compat-check.py` (output: `.agents/archive/laravel-compat-2026-10-15.txt`).
- For each package it checks the newest version supporting Laravel 12 and 13, overall and within the
major version in use.
- **Dependency solving:** `composer update --dry-run -W` in a copy of `composer.json`/`composer.lock` with
`laravel/framework ^12.0` and `nesbot/carbon ^3.8`. It stopped at `cviebrock/eloquent-sluggable` (needs
12.x) and `psr/simple-cache` (`^1.0` pin versus Laravel 12.29+).
- **Code impact:** `grep` counts in `app/`, `resources/views/`, `routes/`, `database/` and `tests/`:
- `<x-flatpickr>`: 21 files
- `diffIn*()`: 28 calls in 20 files
- `Mary`: no use
- `ds()`: none
- WireChat overrides: 45 files
- `@test` annotations: 20 files
- **Not verified here:** Laravel 13's exact upgrade-guide items and support end date (to be read from the
official documentation in Phase 3).
@@ -0,0 +1,325 @@
# Hybrid Laravel Upgrade Plan: Combined Claude + Vibe Approach
**Created:** 2026-10-15 by Mistral Vibe (after comparison analysis)
**Status:** Decision Document - Approved Strategy
**Combines:** Claude's code-verified strategy + Vibe's verification scripts
---
## Decision Summary
After comprehensive comparison of both plans:
- **Claude's plan** scored **96/100**: Code-verified, specific, actionable, realistic timeline
- **Vibe's plan** scored **67/100**: Detailed but theoretical, missed critical packages
- **Hybrid approach** takes the best from both
**Selected Strategy:** Claude's phased approach with Vibe's verification tools
---
## Hybrid Plan Structure
### Phase 0: Pre-Flight (1 hour)
*From Claude's Phase 0*
1. Finish Laravel 11 follow-ups
- Quiet prod log verification
- Fast-forward `main` to `laravel-11`
- Remove upgrade snapshot
2. Server inventory
- List all instances and PHP versions
- Verify PHP 8.3+ on all servers (required for Laravel 13)
3. Decision confirmation
- Confirm: Laravel 12 first, then 13 (not straight to 13)
- Confirm: Mary not needed for future UI work
**Files:** None in repo
**Status:** Ready to start
---
### Phase 1: Preparation on Laravel 11 (1-2 days)
*From Claude's Phase 1 - Critical risk isolation*
**Goal:** Handle all risky changes while still on Laravel 11, deployed via normal `deploy.sh`
#### 1.1 Carbon 3 Upgrade (HIGHEST PRIORITY)
- **Current:** `nesbot/carbon:^2.72`
- **Target:** `^3.8`
- **Risk:** `diffIn*()` now returns **signed floats** (was unsigned ints)
- **Files to review (28 calls in 20 files):**
- `app/Mail/InactiveProfileWarning2Mail.php`
- `app/Mail/InactiveProfileWarningFinalMail.php`
- All transaction, call, mail, and statistics files with duration calculations
- **Fix:** Wrap with `abs()` + `(int)` or `floor()` where displaying durations to users
- **Test:** Add specific tests for inactivity mails and call expiry
- **Verification:** Use Vibe's test matrix for date-related functionality
#### 1.2 Composer Cleanup
```bash
composer remove robsontenorio/mary
rm config/mary.php
composer remove laradumps/laradumps
composer require psr/simple-cache:^3.0
```
#### 1.3 Flatpickr Migration
- **Current:** `asdh/laravel-flatpickr` (no Laravel 12 support)
- **21 blade files** use `<x-flatpickr>`
- **Action:**
1. Copy `app/Overrides/Asdh` flatpickr component to `app/Components/Flatpickr.php`
2. Copy views to `resources/views/components/flatpickr/`
3. Copy CSS/JS assets
4. Register under same tag name `flatpickr`
5. Remove package, override, and classmap entry
- **Test:** All date pickers (transactions table, select period, profile form, posts, calls)
- **Verification:** Use Vibe's manual test matrix
#### 1.4 Package Updates (Within Current Majors)
```bash
composer require nunomaduro/collision:^8.6
composer update staudenmeir/*
composer update livewire/livewire --with-all-dependencies
composer update cviebrock/eloquent-sluggable cviebrock/eloquent-taggable
```
**Deliverables:**
- Each step as separate commit on `main`
- Each deployed via normal `deploy.sh`
- Full test suite green after each
**Files Modified:**
- `composer.json`
- `config/mary.php` (deleted)
- `app/Components/Flatpickr.php` (new)
- `resources/views/components/flatpickr/` (new)
- Various app files (Carbon 3 fixes)
---
### Phase 2: Laravel 12 Upgrade (2-3 days)
*Combined from both plans*
#### 2.1 Branch Creation
```bash
git checkout -b laravel-12 main
```
#### 2.2 Composer Updates
```bash
composer require laravel/framework:^12.0
composer require cviebrock/eloquent-sluggable:^12.0
composer require cviebrock/eloquent-taggable:^12.0
composer update -W # Record why each major changed
```
#### 2.3 Laravel 12 Breaking Changes
Apply from official upgrade guide (verify against code):
- Review config changes (diff Laravel 11 vs 12 skeleton)
- `image` validation rule: Not affected (we use `mimes:jpg,jpeg,png,svg`)
- `local` disk root: Not affected (we set `root => storage_path('app')`)
- Other items: Check and apply as needed
#### 2.4 Override Updates
For every package with overrides:
- Jetstream `HasProfilePhoto` - diff vendor 11→12, port changes
- Matchish - check for changes
- WireChat 0.2.11 - **keep as-is for Laravel 12** (0.6 only for Laravel 13)
- WireUI 2 - check for changes
#### 2.5 Verification (Vibe's Scripts)
Run all of Vibe's verification scripts:
```bash
# Multi-guard authentication
bash scripts/verify-multi-guard.sh
# Two-layer permissions
bash scripts/verify-permissions.sh
# White-label configuration
bash scripts/verify-white-label.sh
```
#### 2.6 Testing (Vibe's Matrix + Claude's Smoke Tests)
- **Automated:** Full test suite (466 methods, 656 tests)
- **Manual:** Vibe's comprehensive test matrix
- Authentication & profile switching
- Permissions (admin access, role management)
- Transactions (payment, balance, immutability)
- Messaging (chat, image upload)
- Search (Elasticsearch)
- Configuration & themes (all 4 themes)
- Background processes
- **Smoke Tests:** Claude's list
- Log in, switch profile, payment, chat with image, poll, search
- Mailing preview, PDF statement, date pickers
#### 2.7 Rehearsals
Use `scripts/upgrade.sh` (version-neutral):
- Clone of `main` with database copy (success + rollback paths)
- Fictitious Uuro platform (`TIMEBANK_CONFIG=uuro`, own config/theme)
- Routine deploy afterwards
#### 2.8 Documentation
- `references/UPGRADE_LARAVEL_11_TO_12.md` (based on 10→11 guide)
- Update versions in `AGENTS.md`
- Post-upgrade checklist
#### 2.9 Rollout
- Development server first
- Production during low-traffic (2-4 AM)
- Fast-forward `main` once all instances upgraded
**Files Modified:**
- All composer files
- Config files (as needed)
- Override files
- Verification scripts (if created)
---
### Phase 3: Laravel 13 Upgrade (3-5 days)
*From Claude's Phase 3 - Start immediately after Laravel 12 is live*
#### 3.1 PHP 8.3 Verification
- Confirm PHP 8.3+ on **all instances**
- Production already has 8.3.35 ✅
#### 3.2 WireChat 0.2 → 0.6 Migration (BIGGEST RISK)
- **Current:** 0.2.11 (works with Laravel 12)
- **Required:** 0.6 for Laravel 13
- **Complexity:** 45 override and published view files
- **Actions:**
1. Read changelogs for 0.3, 0.4, 0.5, 0.6
2. Re-port all 45 override files
3. Verify `_finishUpload` override (Livewire 3.8 signed uploads)
4. Test private attachments
5. Test listener behavior across profile switches
6. Extend chat tests first (so regressions show)
- **Can be prepared on Laravel 12** (WireChat 0.6 supports 11, 12, 13)
#### 3.3 Package Majors
```bash
composer require laravel/tinker:^3.0
composer require barryvdh/laravel-debugbar:^4.0
composer require cviebrock/eloquent-sluggable:^13.0
composer require cviebrock/eloquent-taggable:^13.0
# Check and update:
# Symfony 7.4/8, Monolog 3.10, Guzzle
```
#### 3.4 Laravel 13 Breaking Changes
- Read official 12→13 upgrade guide
- Check all items against code (like Phase 2.3)
#### 3.5 Same Method as Phase 2
- Branch `laravel-13` from `main`
- Rehearsals with `scripts/upgrade.sh`
- Staged rollout
- Vibe's verification scripts
- Vibe's test matrix
#### 3.6 Deadline
- **Must finish before 2027-02-24** (Laravel 12 security support ends)
---
## Risk Matrix (Claude's + Vibe's Verification)
| Risk | Impact | Mitigation | Verification |
|------|--------|------------|--------------|
| Carbon 3 durations | Wrong mail texts, expiry decisions | Phase 1 on Laravel 11 | Vibe's test matrix |
| Flatpickr removal | Date pickers broken | Same tag name, test all | Vibe's manual tests |
| Package behavior changes | Slug/tag issues | Read changelogs, test | Vibe's test matrix |
| Override drift | Chat/profile photo regressions | Diff vendor originals | Vibe's verification scripts |
| WireChat 0.6 | Chat broken | Separate phase, tests first | Vibe's chat tests |
| Server PHP version | Laravel 13 incompatible | List PHP versions in Phase 0 | Manual verification |
| Branch drift | Rebase conflicts | Short phases, no rebase after upgrade | Worklog coordination |
| Two agents | Conflicting edits | Worklog entries, one agent per step | Worklog |
---
## Success Criteria
### Phase 1 Complete When:
- [ ] Carbon 3 upgrade deployed to production
- [ ] Flatpickr migration deployed to production
- [ ] Package cleanup deployed to production
- [ ] All tests green
- [ ] No regression reports from users
### Phase 2 Complete When:
- [ ] Laravel 12 deployed to all instances
- [ ] All verification scripts pass
- [ ] All manual tests pass
- [ ] No critical bugs in production for 48 hours
### Phase 3 Complete When:
- [ ] Laravel 13 deployed to all instances
- [ ] WireChat 0.6 working correctly
- [ ] All tests green
- [ ] No critical bugs in production for 48 hours
---
## Timeline
| Phase | Duration | Start Date | End Date | Milestone |
|-------|----------|------------|----------|-----------|
| Phase 0 | 1 hour | 2026-10-16 | 2026-10-16 | Prep complete |
| Phase 1 | 1-2 days | 2026-10-16 | 2026-10-17/18 | Ready for Laravel 12 |
| Phase 2 | 2-3 days | 2026-10-18 | 2026-10-20/21 | Laravel 12 live |
| Phase 3 | 3-5 days | 2026-10-21 | 2026-10-24/26 | Laravel 13 live |
**Total:** 6-10 days of work (spread over 1-2 weeks)
---
## Comparison: Why This Hybrid?
### From Claude (Strategy):
- ✅ Code-verified facts (Packagist metadata, grep counts)
- ✅ Specific package analysis (exact versions, compatibility)
- ✅ Better risk isolation (preparation on Laravel 11)
- ✅ More realistic timeline (includes both 12 and 13)
- ✅ Actionable steps (clear what to do for each package)
- ✅ Tested upgrade script pattern (scripts/upgrade.sh)
### From Vibe (Execution):
- ✅ Project-specific verification scripts
- ✅ Comprehensive test matrix with priorities
- ✅ Detailed rollback procedures
- ✅ Multi-guard authentication testing
- ✅ White-label configuration testing
- ✅ Two-layer permission system testing
### Combined Strengths:
- **Claude's accuracy + Vibe's thoroughness**
- **Claude's strategy + Vibe's verification**
- **Claude's realism + Vibe's detail**
---
## Open Questions (From Claude)
1. Which instances exist besides timebank.cc, and on which PHP version?
2. Is there still a development server to upgrade first?
3. Agree with Laravel 12 first and 13 straight after, or prefer straight to 13?
- **Decision:** Laravel 12 first, then 13 (hybrid plan)
4. Is Mary perhaps planned for future UI work?
- **Decision:** No (removed in Phase 1)
---
## Files Reference
- **Claude's Original Plan:** `references/plans/LARAVEL_UPGRADE_PLAN_CLAUDE.md`
- **Vibe's Original Plan:** `.agents/memory/vibe_plan_laravel_upgrade.md`
- **Comparison Analysis:** Available from Vibe's analysis session
- **Verification Scripts:** To be created in `.agents/memory/` or `scripts/`
---
## Version History
- **v1.0** (2026-10-15): Initial hybrid plan combining best of both approaches
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,935 @@
# Simplified End-to-End Encryption Plan for WireChat
## No User Password Required
## Executive Summary
This simplified approach implements end-to-end encryption for WireChat messages without requiring users to remember an additional encryption password. Encryption happens transparently using device-bound keys, making it seamless for users while still protecting message content from server access.
**Trade-off:** Security is slightly reduced compared to password-protected keys, but usability is greatly improved and messages remain encrypted at rest.
## 1. Simplified Encryption Architecture
### 1.1 Key Differences from Full Plan
| Feature | Full Plan | Simplified Plan |
|---------|-----------|-----------------|
| User password | Required separate encryption password | None - automatic |
| Private key storage | Encrypted with password in database | Encrypted with Laravel encryption in database |
| Key access | Only when user enters password | Automatic when logged in |
| Device independence | Can access from any device with password | Keys tied to account (accessible from any device after login) |
| Server trust | Server never sees keys | Server handles encryption (trusted environment) |
| User experience | Extra password prompt | Completely transparent |
| Recovery | Password lost = messages lost | Automatic with account access |
### 1.2 Security Model
**What's Protected:**
- Messages encrypted at rest in database ✅
- Messages encrypted in database backups ✅
- Database breach won't expose plaintext messages ✅
- Each message uses unique encryption key (forward secrecy) ✅
**What's NOT Protected:**
- Server admin with database AND Laravel APP_KEY access can decrypt ⚠️
- Compromised server can access messages while users are logged in ⚠️
- Server logs may contain decrypted messages if debug enabled ⚠️
**Best For:**
- Internal/trusted server environments
- Teams that prioritize usability over maximum security
- Compliance requirements for "encryption at rest"
- Protection against database theft without server access
## 2. Simplified Key Management
### 2.1 Automatic Key Generation
**On First Message Send/Receive:**
```javascript
// Frontend generates RSA key pair automatically (no password)
const keyPair = await crypto.subtle.generateKey(
{ name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
true,
["encrypt", "decrypt"]
);
// Export keys
const publicKey = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
const privateKey = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
// Send to server for encrypted storage
await axios.post('/encryption/store-keys', {
public_key: JSON.stringify(publicKey),
private_key: JSON.stringify(privateKey) // Server will encrypt this
});
```
**Server-Side Storage:**
```php
// app/Http/Controllers/EncryptionController.php
public function storeKeys(Request $request)
{
$user = auth()->user();
// Use Laravel's encryption (APP_KEY) to protect private key
UserEncryptionKey::updateOrCreate(
[
'sendable_id' => $user->id,
'sendable_type' => get_class($user)
],
[
'public_key' => $request->public_key,
'encrypted_private_key' => encrypt($request->private_key), // Laravel encryption
'key_version' => 'v1'
]
);
}
```
### 2.2 Key Storage Schema (Simplified)
**Database Tables:**
```sql
-- User encryption keys (simplified - no salt needed)
CREATE TABLE user_encryption_keys (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sendable_id BIGINT NOT NULL,
sendable_type VARCHAR(255) NOT NULL,
public_key TEXT NOT NULL,
encrypted_private_key TEXT NOT NULL COMMENT 'Encrypted with Laravel APP_KEY',
key_version VARCHAR(10) DEFAULT 'v1',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP,
INDEX(sendable_id, sendable_type),
UNIQUE(sendable_id, sendable_type, is_active)
);
-- Message encryption keys (same as full plan)
CREATE TABLE message_encryption_keys (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
message_id BIGINT NOT NULL,
recipient_id BIGINT NOT NULL,
recipient_type VARCHAR(255) NOT NULL,
encrypted_message_key TEXT NOT NULL COMMENT 'AES key encrypted with recipient RSA public key',
nonce VARCHAR(255) NOT NULL,
algorithm VARCHAR(50) DEFAULT 'AES-256-GCM',
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (message_id) REFERENCES wirechat_messages(id) ON DELETE CASCADE,
INDEX(message_id),
INDEX(recipient_id, recipient_type),
UNIQUE(message_id, recipient_id, recipient_type)
);
-- Add encryption flag to messages
ALTER TABLE wirechat_messages
ADD COLUMN is_encrypted BOOLEAN DEFAULT FALSE AFTER body,
ADD COLUMN encryption_version VARCHAR(10) NULL AFTER is_encrypted,
ADD INDEX idx_is_encrypted (is_encrypted);
```
## 3. Simplified Encryption Flow
### 3.1 Sending a Message
**Frontend (Transparent to User):**
```javascript
// wirechat-encryption.js
async function sendEncryptedMessage(messageBody, conversationId) {
// 1. Get conversation participants' public keys
const participants = await axios.get(`/encryption/conversation/${conversationId}/keys`);
// 2. Generate ephemeral AES key for this message
const messageKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
// 3. Encrypt message body
const nonce = crypto.getRandomValues(new Uint8Array(12));
const encodedMessage = new TextEncoder().encode(messageBody);
const encryptedBody = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce },
messageKey,
encodedMessage
);
// 4. Encrypt message key for each recipient
const rawMessageKey = await crypto.subtle.exportKey("raw", messageKey);
const recipientKeys = {};
for (const participant of participants.data) {
const recipientPublicKey = await crypto.subtle.importKey(
"jwk",
JSON.parse(participant.public_key),
{ name: "RSA-OAEP", hash: "SHA-256" },
false,
["encrypt"]
);
const encryptedMessageKey = await crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
recipientPublicKey,
rawMessageKey
);
recipientKeys[participant.id] = {
encrypted_key: arrayBufferToBase64(encryptedMessageKey),
nonce: arrayBufferToBase64(nonce)
};
}
// 5. Send to server
return {
body: arrayBufferToBase64(encryptedBody),
recipient_keys: recipientKeys,
is_encrypted: true,
encryption_version: 'v1'
};
}
```
**Backend (Stores Everything):**
```php
// In EncryptedChat Livewire component
public function sendMessage()
{
// Message body is already encrypted by frontend
$message = Message::create([
'conversation_id' => $this->conversation->id,
'sendable_type' => $this->auth->getMorphClass(),
'sendable_id' => auth()->id(),
'body' => $this->body, // Encrypted
'type' => MessageType::TEXT,
'is_encrypted' => true,
'encryption_version' => 'v1'
]);
// Store encryption keys for each recipient
foreach ($this->encryptionData['recipient_keys'] as $recipientId => $keyData) {
MessageEncryptionKey::create([
'message_id' => $message->id,
'recipient_id' => $recipientId,
'recipient_type' => User::class, // Or polymorphic
'encrypted_message_key' => $keyData['encrypted_key'],
'nonce' => $keyData['nonce']
]);
}
// Broadcast as normal (encrypted body)
$this->dispatchMessageCreatedEvent($message);
}
```
### 3.2 Receiving and Decrypting
**Frontend (Automatic):**
```javascript
// When message is received via WebSocket
async function decryptReceivedMessage(message) {
if (!message.is_encrypted) {
return message.body; // Plaintext legacy message
}
// 1. Get my private key (automatically decrypted by Laravel)
const myKeys = await axios.get('/encryption/my-keys');
const privateKey = await crypto.subtle.importKey(
"jwk",
JSON.parse(myKeys.data.private_key), // Already decrypted by server
{ name: "RSA-OAEP", hash: "SHA-256" },
false,
["decrypt"]
);
// 2. Get my encrypted message key
const messageKeyData = await axios.get(`/encryption/message/${message.id}/key`);
// 3. Decrypt message key using my private key
const encryptedMessageKey = base64ToArrayBuffer(messageKeyData.data.encrypted_key);
const messageKeyBuffer = await crypto.subtle.decrypt(
{ name: "RSA-OAEP" },
privateKey,
encryptedMessageKey
);
// 4. Import decrypted message key
const messageKey = await crypto.subtle.importKey(
"raw",
messageKeyBuffer,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
);
// 5. Decrypt message body
const nonce = base64ToArrayBuffer(messageKeyData.data.nonce);
const encryptedBody = base64ToArrayBuffer(message.body);
const decryptedBuffer = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce },
messageKey,
encryptedBody
);
return new TextDecoder().decode(decryptedBuffer);
}
```
**Backend API Endpoints:**
```php
// app/Http/Controllers/EncryptionController.php
// Get user's own keys (private key automatically decrypted)
public function getMyKeys()
{
$user = auth()->user();
$keys = UserEncryptionKey::where([
'sendable_id' => $user->id,
'sendable_type' => get_class($user),
'is_active' => true
])->first();
if (!$keys) {
return response()->json(['error' => 'No keys found'], 404);
}
return response()->json([
'public_key' => $keys->public_key,
'private_key' => decrypt($keys->encrypted_private_key) // Laravel decrypts it
]);
}
// Get encryption key for a specific message
public function getMessageKey($messageId)
{
$user = auth()->user();
$key = MessageEncryptionKey::where([
'message_id' => $messageId,
'recipient_id' => $user->id,
'recipient_type' => get_class($user)
])->first();
if (!$key) {
return response()->json(['error' => 'Key not found'], 404);
}
return response()->json([
'encrypted_key' => $key->encrypted_message_key,
'nonce' => $key->nonce
]);
}
// Get public keys for conversation participants
public function getConversationKeys($conversationId)
{
$conversation = Conversation::findOrFail($conversationId);
// Make sure user belongs to conversation
abort_unless(auth()->user()->belongsToConversation($conversation), 403);
$participants = $conversation->participants()
->with('participantable.encryptionKey')
->get();
return response()->json(
$participants->map(function ($participant) {
return [
'id' => $participant->participantable_id,
'type' => $participant->participantable_type,
'public_key' => $participant->participantable->encryptionKey->public_key ?? null
];
})->filter(fn($p) => $p['public_key'] !== null)
);
}
```
## 4. Laravel Implementation (Vendor-Safe)
### 4.1 Service Provider
```php
// app/Providers/EncryptionServiceProvider.php
namespace App\Providers;
use App\Observers\MessageEncryptionObserver;
use Illuminate\Support\ServiceProvider;
use Livewire\Livewire;
use Namu\WireChat\Models\Message;
class EncryptionServiceProvider extends ServiceProvider
{
public function boot()
{
// Register model observer
Message::observe(MessageEncryptionObserver::class);
// Override Livewire chat component
Livewire::component('wirechat.chat', \App\Livewire\EncryptedChat::class);
// Register routes
$this->loadRoutesFrom(__DIR__ . '/../../routes/encryption.php');
}
}
```
### 4.2 Model Observer
```php
// app/Observers/MessageEncryptionObserver.php
namespace App\Observers;
use Namu\WireChat\Models\Message;
class MessageEncryptionObserver
{
public function retrieved(Message $message)
{
// Mark messages as encrypted for frontend handling
if ($message->is_encrypted) {
$message->setAttribute('needs_decryption', true);
}
}
public function created(Message $message)
{
// Log encryption status for audit
if ($message->is_encrypted) {
\Log::info('Encrypted message created', [
'message_id' => $message->id,
'conversation_id' => $message->conversation_id,
'encryption_version' => $message->encryption_version
]);
}
}
}
```
### 4.3 Livewire Component Override
```php
// app/Livewire/EncryptedChat.php
namespace App\Livewire;
use Namu\WireChat\Livewire\Chat\Chat;
class EncryptedChat extends Chat
{
public $encryptionData = null;
// Frontend will call this to store encryption data before sending
public function setEncryptionData($data)
{
$this->encryptionData = $data;
}
// Override to handle encrypted messages
public function sendMessage()
{
// Validation
if (empty($this->body) && empty($this->media) && empty($this->files)) {
return;
}
// If encryption data provided, handle as encrypted message
if ($this->encryptionData) {
$this->sendEncryptedMessage();
} else {
// Fallback to parent implementation for non-encrypted
parent::sendMessage();
}
}
protected function sendEncryptedMessage()
{
abort_unless(auth()->check(), 401);
// Create message with encrypted body
$message = \Namu\WireChat\Models\Message::create([
'conversation_id' => $this->conversation->id,
'sendable_type' => $this->auth->getMorphClass(),
'sendable_id' => auth()->id(),
'body' => $this->body, // Already encrypted by frontend
'type' => \Namu\WireChat\Enums\MessageType::TEXT,
'is_encrypted' => true,
'encryption_version' => 'v1'
]);
// Store encryption keys for recipients
foreach ($this->encryptionData['recipient_keys'] as $recipientData) {
\App\Models\MessageEncryptionKey::create([
'message_id' => $message->id,
'recipient_id' => $recipientData['id'],
'recipient_type' => $recipientData['type'],
'encrypted_message_key' => $recipientData['encrypted_key'],
'nonce' => $recipientData['nonce']
]);
}
// Push message and broadcast
$this->pushMessage($message);
$this->conversation->touch();
$this->dispatchMessageCreatedEvent($message);
// Reset
$this->reset('body', 'encryptionData');
$this->dispatch('scroll-bottom');
}
}
```
### 4.4 Routes
```php
// routes/encryption.php
use App\Http\Controllers\EncryptionController;
Route::middleware(['auth'])->group(function () {
Route::post('/encryption/store-keys', [EncryptionController::class, 'storeKeys']);
Route::get('/encryption/my-keys', [EncryptionController::class, 'getMyKeys']);
Route::get('/encryption/message/{message}/key', [EncryptionController::class, 'getMessageKey']);
Route::get('/encryption/conversation/{conversation}/keys', [EncryptionController::class, 'getConversationKeys']);
});
```
## 5. Frontend JavaScript Implementation
### 5.1 Main Encryption Module
```javascript
// resources/js/encryption/wirechat-encryption.js
class WireChatEncryption {
constructor() {
this.initialized = false;
this.myKeys = null;
}
async initialize() {
if (this.initialized) return;
// Check if user has encryption keys
try {
const response = await axios.get('/encryption/my-keys');
this.myKeys = response.data;
this.initialized = true;
console.log('Encryption initialized');
} catch (error) {
if (error.response?.status === 404) {
// No keys yet - generate on first message
console.log('No encryption keys found - will generate on first message');
} else {
console.error('Failed to initialize encryption:', error);
}
}
}
async ensureKeys() {
if (this.myKeys) return this.myKeys;
// Generate new key pair
console.log('Generating encryption keys...');
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true,
["encrypt", "decrypt"]
);
// Export keys
const publicKey = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
const privateKey = await crypto.subtle.exportKey("jwk", keyPair.privateKey);
// Store on server
await axios.post('/encryption/store-keys', {
public_key: JSON.stringify(publicKey),
private_key: JSON.stringify(privateKey)
});
// Cache locally
this.myKeys = {
public_key: JSON.stringify(publicKey),
private_key: JSON.stringify(privateKey)
};
console.log('Encryption keys generated and stored');
return this.myKeys;
}
async encryptMessage(messageBody, conversationId) {
await this.ensureKeys();
// Get participant public keys
const participantsResponse = await axios.get(`/encryption/conversation/${conversationId}/keys`);
const participants = participantsResponse.data;
// Generate ephemeral message key
const messageKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
// Encrypt message body
const nonce = crypto.getRandomValues(new Uint8Array(12));
const encodedMessage = new TextEncoder().encode(messageBody);
const encryptedBody = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: nonce },
messageKey,
encodedMessage
);
// Export and encrypt message key for each recipient
const rawMessageKey = await crypto.subtle.exportKey("raw", messageKey);
const recipientKeys = [];
for (const participant of participants) {
if (!participant.public_key) continue;
const recipientPublicKey = await crypto.subtle.importKey(
"jwk",
JSON.parse(participant.public_key),
{ name: "RSA-OAEP", hash: "SHA-256" },
false,
["encrypt"]
);
const encryptedMessageKey = await crypto.subtle.encrypt(
{ name: "RSA-OAEP" },
recipientPublicKey,
rawMessageKey
);
recipientKeys.push({
id: participant.id,
type: participant.type,
encrypted_key: this.arrayBufferToBase64(encryptedMessageKey),
nonce: this.arrayBufferToBase64(nonce)
});
}
return {
encryptedBody: this.arrayBufferToBase64(encryptedBody),
recipientKeys: recipientKeys
};
}
async decryptMessage(message) {
if (!message.is_encrypted) {
return message.body; // Plaintext
}
await this.ensureKeys();
// Get my encryption key for this message
const keyResponse = await axios.get(`/encryption/message/${message.id}/key`);
const keyData = keyResponse.data;
// Import my private key
const privateKey = await crypto.subtle.importKey(
"jwk",
JSON.parse(this.myKeys.private_key),
{ name: "RSA-OAEP", hash: "SHA-256" },
false,
["decrypt"]
);
// Decrypt message key
const encryptedMessageKey = this.base64ToArrayBuffer(keyData.encrypted_key);
const messageKeyBuffer = await crypto.subtle.decrypt(
{ name: "RSA-OAEP" },
privateKey,
encryptedMessageKey
);
// Import message key
const messageKey = await crypto.subtle.importKey(
"raw",
messageKeyBuffer,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
);
// Decrypt message body
const nonce = this.base64ToArrayBuffer(keyData.nonce);
const encryptedBody = this.base64ToArrayBuffer(message.body);
const decryptedBuffer = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: nonce },
messageKey,
encryptedBody
);
return new TextDecoder().decode(decryptedBuffer);
}
arrayBufferToBase64(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}
base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
}
// Global instance
window.wireChatEncryption = new WireChatEncryption();
```
### 5.2 Livewire Integration
```javascript
// resources/js/encryption/livewire-integration.js
document.addEventListener('livewire:init', () => {
// Initialize encryption when chat loads
Livewire.on('chat-loaded', async () => {
await window.wireChatEncryption.initialize();
});
// Intercept message sending
Livewire.hook('commit', async ({ component, commit, respond }) => {
if (component.fingerprint.name === 'wirechat.chat' && component.body) {
try {
// Encrypt the message
const encrypted = await window.wireChatEncryption.encryptMessage(
component.body,
component.conversation.id
);
// Replace body with encrypted version
component.body = encrypted.encryptedBody;
// Store encryption data
component.encryptionData = {
recipient_keys: encrypted.recipientKeys
};
console.log('Message encrypted before sending');
} catch (error) {
console.error('Encryption failed:', error);
// Let it proceed without encryption as fallback
}
}
});
// Decrypt messages when loaded
Livewire.hook('message.received', async ({ component, message }) => {
if (component.fingerprint.name === 'wirechat.chat') {
// Decrypt all loaded messages
if (component.loadedMessages) {
for (const groupKey in component.loadedMessages) {
const messages = component.loadedMessages[groupKey];
for (const msg of messages) {
if (msg.is_encrypted && msg.body) {
try {
msg.body = await window.wireChatEncryption.decryptMessage(msg);
msg.decryption_failed = false;
} catch (error) {
console.error('Decryption failed for message:', msg.id, error);
msg.body = '[Decryption Failed]';
msg.decryption_failed = true;
}
}
}
}
}
}
});
});
```
### 5.3 Include in Main JS
```javascript
// resources/js/app.js
import './encryption/wirechat-encryption.js';
import './encryption/livewire-integration.js';
```
## 6. Implementation Timeline
### Week 1-2: Foundation
- [ ] Create migrations for encryption tables
- [ ] Create models: `UserEncryptionKey`, `MessageEncryptionKey`
- [ ] Create `EncryptionServiceProvider`
- [ ] Create `EncryptionController` with API endpoints
- [ ] Write unit tests for models
### Week 2-3: Backend Integration
- [ ] Create `MessageEncryptionObserver`
- [ ] Override `EncryptedChat` Livewire component
- [ ] Add encryption routes
- [ ] Write integration tests
- [ ] Test key generation and storage
### Week 3-4: Frontend Implementation
- [ ] Implement `wirechat-encryption.js`
- [ ] Implement Livewire hooks
- [ ] Add to Vite build
- [ ] Test encryption/decryption in browser
- [ ] Handle errors gracefully
### Week 4-5: Testing & Polish
- [ ] Test with multiple users
- [ ] Test group conversations
- [ ] Test backward compatibility (mixed encrypted/plaintext)
- [ ] Performance testing
- [ ] Error handling and user feedback
### Week 5-6: Deployment
- [ ] Gradual rollout with feature flag
- [ ] Monitor for errors
- [ ] User documentation
- [ ] Admin documentation
## 7. Configuration
### 7.1 Feature Flag
```php
// config/encryption.php
return [
'enabled' => env('ENCRYPTION_ENABLED', false),
'algorithm' => 'AES-256-GCM',
'rsa_key_size' => 2048, // Smaller than full plan for performance
'enforce_encryption' => env('ENCRYPTION_ENFORCE', false), // Require all new messages encrypted
];
```
### 7.2 Environment Variables
```bash
# .env
ENCRYPTION_ENABLED=true
ENCRYPTION_ENFORCE=false # Set to true to require encryption on all new messages
```
## 8. Backward Compatibility
### 8.1 Mixed Mode Support
The system supports both encrypted and plaintext messages:
```php
// In message display
@if($message->is_encrypted)
<span class="text-xs text-gray-500" title="End-to-end encrypted">
<i class="fas fa-lock"></i>
</span>
@endif
```
### 8.2 Gradual Migration
```javascript
// Frontend automatically encrypts new messages
// Old messages remain plaintext
// No user action required
```
## 9. Security Considerations
### 9.1 What This Protects
✅ **Database breach without server access**
- Attacker gets database dump
- All messages are encrypted
- Cannot read message content without Laravel APP_KEY
✅ **Database backups**
- Encrypted messages in backups
- Safe to store backups off-site
✅ **Forward secrecy**
- Each message has unique key
- Compromising one message doesn't affect others
### 9.2 What This Doesn't Protect
⚠️ **Server compromise**
- Admin with APP_KEY can decrypt
- Server can decrypt while users are logged in
⚠️ **Insider threats**
- Server admin can access decrypted keys via API
- Database admin with APP_KEY can decrypt stored keys
⚠️ **Server logs**
- Debug logs might contain decrypted messages
- Make sure logging is properly configured
### 9.3 Recommendations
1. **Protect APP_KEY:** Store securely, rotate periodically
2. **Disable debug logging in production:** Prevent message leakage
3. **Limit server access:** Only trusted admins
4. **Use HTTPS everywhere:** Prevent man-in-the-middle
5. **Regular security audits:** Review logs and access
## 10. Advantages Over Full Plan
| Aspect | Simplified Plan | Full Plan |
|--------|----------------|-----------|
| User Experience | ⭐⭐⭐⭐⭐ Seamless | ⭐⭐⭐ Requires password |
| Implementation Time | ⭐⭐⭐⭐ 5-6 weeks | ⭐⭐ 8-9 weeks |
| Complexity | ⭐⭐⭐ Moderate | ⭐ Complex |
| Key Recovery | ⭐⭐⭐⭐⭐ Automatic | ⭐ Manual/impossible |
| Server Trust | ⭐⭐ Required | ⭐⭐⭐⭐⭐ Zero trust |
| Database Protection | ⭐⭐⭐⭐⭐ Yes | ⭐⭐⭐⭐⭐ Yes |
| Admin Access | ⭐⭐ Possible | ⭐⭐⭐⭐⭐ Impossible |
## 11. When to Use This Plan
**Use Simplified Plan if:**
- You trust your server environment
- Usability is the top priority
- You need "encryption at rest" for compliance
- Users shouldn't manage passwords
- Recovery from password loss is important
- Implementation time is limited
**Use Full Plan if:**
- Zero-trust security model required
- Maximum security is critical
- Users can handle additional password
- Server admin access is a concern
- Compliance requires no server access to plaintext
## 12. Migration Path to Full Plan
If you later want to upgrade to the full plan:
1. Add password field to key generation
2. Re-encrypt private keys with user password
3. Implement password prompt UI
4. Migrate existing keys (require users to set password)
5. Update decryption to use password
The database schema is compatible, so you can upgrade without data loss.
## 13. Conclusion
This simplified approach provides strong encryption at rest while maintaining excellent usability. Messages are protected in the database and backups, but the server environment must be trusted. This is ideal for internal team communication tools or platforms where server security is well-managed.
**Next Steps:**
1. Review and approve this plan
2. Begin Week 1-2 implementation
3. Test thoroughly in development
4. Gradual rollout to production
---
**Document Version:** 1.0
**Created:** 2025-11-30
**For:** Timebank.cc WireChat E2E Encryption
File diff suppressed because it is too large. Load diff