Fictional example. Northwind Bookings is an invented company, and every name, file path, number and finding here is made up to show the format and level of detail you get. Prices are current list prices.
Codebase Assessment: Northwind Bookings
- Client
- Northwind Bookings (fictional)
- App
- Appointment-booking SaaS for small service businesses
- Stack
- Laravel 10.48, PHP 8.1, Vue 3, MySQL 8.0, Laravel Nova 4 admin
- Size
- About 40k lines in one repository (PHP, Vue, Blade)
- Hosting
- One VPS running Ubuntu 22.04: nginx, PHP-FPM, MySQL and the queue worker
- Review
- Codebase Assessment, Standard (code plus hosting and deploy)
- Access used
- Read-only repository and server access. No production data copied.
- Delivered
- Fri 9 Oct 2026
Summary
Northwind Bookings works and is worth keeping. The code follows normal Laravel conventions, and a new developer could find their way around it in days, not weeks. It is not safe to leave as it is, though. Three problems need attention now. Live payment and email passwords are still readable in the code history. Any logged-in customer can cancel another customer’s booking. And the nightly backups sit on the same server they protect and have never been restored. Beyond those three, the app runs on PHP and Laravel versions that no longer get security fixes, very little of the booking flow is covered by tests, and nothing alerts anyone when the app or its reminder queue stops. None of this calls for a rebuild. I recommend fixing it in three phases: urgent risks first, then an upgrade to supported versions, then tests and monitoring. Each phase has a fixed price below.
Scorecard
| Security | At risk | Secrets in git history, a missing ownership check on cancel, and open mass assignment on booking updates. |
| Versions and dependencies | At risk | PHP 8.1 and Laravel 10 no longer get security fixes. composer audit lists open advisories. |
| Data safety | At risk | Backups run nightly but stay on the same disk and have never been restored. |
| Tests | Needs work | 12% coverage on the booking flow and 4 tests failing since February 2025. |
| Deploy and operations | Needs work | Manual deploys from a laptop, no CI, an unsupervised queue worker and no alerts. |
| Performance | Fair | Fine for current traffic. The admin bookings list and the availability check slow down as data grows. |
| Code structure | Good | Thin controllers, service classes for availability and payments, and consistent naming. Easy to hand over. |
| Documentation | Fair | The README gets a local copy running. There is no deploy or recovery runbook. |
Findings, most urgent first
Live secrets are still in the git history
- Evidence
- gitleaks finds a .env.production file added in commit 3f9c2ab (Mar 2023) and deleted in 7d01e4c. It still holds the live Stripe secret key, the SMTP password and APP_KEY. config/services.php:31 also hardcodes the SMS API token as an env() fallback. I list locations only; no values are copied into this report.
- Impact
- Anyone who has ever cloned the repository, including former developers and contractors, can take payments, send email as you and decrypt customer phone numbers.
- Fix
- Rotate the Stripe, SMTP and SMS credentials now. Rotate APP_KEY carefully: customers.phone uses an encrypted cast, so rotate after the Laravel 11 step using APP_PREVIOUS_KEYS, or re-encrypt with a one-off script first. Remove the hardcoded fallback and add a gitleaks check to CI.
Any logged-in customer can cancel any booking
- Evidence
- routes/api.php:41 maps POST /api/bookings/{booking}/cancel to BookingController::cancel. The route has the auth middleware, but BookingController.php:132 never checks that the booking belongs to the user, and there is no BookingPolicy. Booking IDs are sequential.
- Impact
- A customer, or a script with one customer account, can cancel other businesses’ appointments by counting through IDs.
- Fix
- Add a BookingPolicy covering view, update and cancel, and call it from every booking route. Add feature tests that prove a second user gets a 403.
Backups stay on the same server and have never been restored
- Evidence
- /etc/cron.d/nwb-backup runs mysqldump nightly into /var/backups/mysql on the same VPS, without --single-transaction. Nothing is copied offsite, uploaded files in storage/app/public are not backed up, and no restore is recorded anywhere.
- Impact
- If the VPS disk fails or the provider account is lost, every booking and customer record goes with it. The dumps may also be inconsistent, because they are taken while bookings are being written.
- Fix
- Encrypted offsite backups of the database and uploads, a consistent dump, a restore drill on a spare server, and a one-page runbook.
PHP 8.1 and Laravel 10 no longer get security fixes
- Evidence
- php -v on the server reports 8.1. composer.json requires laravel/framework ^10.48. PHP 8.1 security support ended 31 Dec 2025, and Laravel 10 security fixes ended 4 Feb 2025. MySQL 8.0 also reached end of life on 30 Apr 2026, which is lower urgency for a database that isn’t exposed to the internet.
- Impact
- New vulnerabilities in PHP or Laravel will not be patched. Customer security questionnaires will flag the versions, and newer packages already refuse to install.
- Fix
- Upgrade Laravel 10 → 11 → 12 → 13, one version at a time, and move PHP to 8.4, installed side by side so rollback is quick. Upgrade MySQL to 8.4 LTS later, as a separate step.
Open security advisories in PHP and JavaScript packages
- Evidence
- composer audit reports 5 advisories in 3 packages, 2 rated high, including the PDF invoice package and the HTTP client used for SMS reminders. npm audit reports 2 high advisories, both in build-time tooling. The iCal export package has had no release since 2022. The Nova 4 license expired in June 2026.
- Impact
- Some advisories are reachable from public pages. The expired Nova license blocks the Nova 5 download that Laravel 12 and 13 need.
- Fix
- Update packages during the upgrade and replace the iCal package with a small in-app class. Renew Nova before the upgrade starts.
Booking updates accept any field (mass assignment)
- Evidence
- app/Models/Booking.php:14 sets $guarded = []. app/Http/Controllers/BookingController.php:88 calls $booking->update($request->all()).
- Impact
- A customer rescheduling a booking can also send price_cents, status or business_id and have them saved. That means free bookings, or a booking moved into another business.
- Fix
- Add an UpdateBookingRequest with explicit rules, use $request->validated(), and replace $guarded with a $fillable list. Add a test that extra fields are ignored.
The queue worker isn’t supervised
- Evidence
- deploy.sh:22 starts the worker with nohup php artisan queue:work &. There is no systemd or Supervisor unit. storage/logs shows no worker output from 14 to 16 Aug 2026, after a provider reboot, and 1,284 reminder jobs were sent late when someone restarted it by hand.
- Impact
- Reminder emails and SMS stop silently after any reboot or crash. Missed reminders mean no-shows for your customers.
- Fix
- A systemd unit with automatic restart, php artisan queue:restart in the deploy, and a heartbeat that alerts when the queue stops moving.
The booking flow is 12% covered, and 4 tests fail
- Evidence
- phpunit --coverage-text: 31 tests, 4 failing in tests/Feature/ReminderTest.php (broken since Feb 2025). app/Services/AvailabilityService.php has 0% coverage and BookingController.php has 9%. Nothing tests double-booking or time zones.
- Impact
- Every change, the upgrade included, is checked by hand or not at all. Double-booking bugs would reach customers first.
- Fix
- Before the upgrade, add smoke tests for signup, login, book, reschedule, cancel and deposit payment. Afterward, add fuller tests for availability and double-booking.
N+1 queries on the admin bookings list
- Evidence
- app/Http/Controllers/Admin/BookingIndexController.php:37 loads bookings without eager loading. BookingResource then reads customer, service and staff for each row. On a staging copy, one page of 50 rows runs 152 queries and takes about 1.9 s.
- Impact
- The page your customers’ staff use most gets slower every month as bookings grow.
- Fix
- Eager-load the relations with with(), select only the needed columns, and add a test that fails if the query count goes above a set limit.
Missing index for the availability check
- Evidence
- database/migrations/2022_05_03_120000_create_bookings_table.php indexes business_id alone. AvailabilityService.php:61 filters on business_id, staff_id and starts_at. EXPLAIN shows a scan of about 410k rows for the largest business.
- Impact
- Booking pages slow down for your biggest customers first, and the slow query holds locks while others are booking.
- Fix
- Add a composite index on (business_id, staff_id, starts_at) with an online migration, then check the query plan again.
No CI; deploys run by hand from a laptop
- Evidence
- There is no .github/workflows directory. deploy.sh runs git pull, composer install and php artisan migrate --force on the server with no test run and no backup first. The last three deploys came from two different laptops.
- Impact
- Broken code can reach production without anyone running the tests, and a failed migration has no rollback.
- Fix
- Run tests and a build on every push, and make deploys run only after the tests pass. Take a database dump before migrations.
No monitoring, and the error log is never rotated
- Evidence
- No uptime check, error tracking or disk alert exists. LOG_CHANNEL=single, and storage/logs/laravel.log is 1.3 GB. The disk is 81% full.
- Impact
- You hear about outages from customers. A full disk would stop MySQL and the app together.
- Fix
- Uptime and certificate checks, a queue heartbeat, disk and memory alerts sent to you first, and the daily log channel with retention.
Recommended plan, fixed prices
Phase 1: Close the urgent risks (start now, 1–2 weeks)
$1,040- Remediation Sprint, Priority fixes: F01 secret rotation, F02 booking policy, F06 mass assignment, F07 supervised queue worker ($650)
- Backup & Disaster Recovery, Starter: F03 encrypted offsite backups, one restore drill, runbook ($390)
Phase 2: Move to supported versions (3–4 weeks)
$1,950, less the $910 credit: $1,040 due- Laravel Upgrade, Plus: 10 → 11 → 12 → 13 with PHP 8.4 and smoke tests on the booking flow (F04, F05, F08) ($1,560)
- CI/CD Pipeline, Starter: F11 tests and build on every push ($390)
- The $910 assessment fee is credited here if this phase is approved by 8 Nov 2026. That leaves $1,040 to pay.
Phase 3: Keep it healthy (when it suits your budget)
$2,080, or any item on its own- Test Coverage for Critical Flows, Starter: availability, double-booking and deposit payment in CI ($780)
- Monitoring & Alerting, Starter: F12 uptime, heartbeats, server alerts, runbook ($260)
- Remediation Sprint, Priority fixes: F09 N+1 queries, F10 availability index ($650)
- End-of-Life Runtime Upgrade, Starter: MySQL 8.0 → 8.4 LTS ($390)
After handover (optional)
$455/mo- Monthly Care Plan, Standard: patches, a monthly restore check, a short report and 7 hours a month (2 routine + 5 change), with same-working-day replies. Minimum 3 months.
Next steps
- Read the summary and the three Critical findings. Rotating the secrets in F01 is worth doing today, whatever you decide about the rest.
- Send questions by email. Follow-up is included until Fri 16 Oct, plus one optional 30-minute call.
- Tell me which phases you want. A fixed proposal for Phase 2 is attached, and I can send one for Phase 1 the same day.
- Renew the Nova license before the upgrade starts (F05).
- Share this report with any developer you hire. It’s yours.