What's New
New features and breaking changes in phpvms 8, written for upgrades from 7.x.
phpvms 8 is a major rewrite: the admin panel is now built on Filament 5, the underlying stack moved from Laravel 10 to Laravel 13 and PHP 8.4, and the module system was replaced. Most changes are additive, so your pilots and API clients keep working. A few things still need attention before or during the upgrade.
This page covers the new features, the smaller additions, and the breaking changes for someone upgrading from 7.x. For the step-by-step update procedure, see Updating.
Before you upgrade
Three things to handle up front:
- PHP 8.4.1 or newer is required - 7.x ran on PHP 8.1+. See Requirements.
- Take a database backup. One migration permanently deletes orphaned
acarsrows (see Database and migrations). - Run the flight-time backfill command after upgrading:
php artisan flights:migrate-time-columns
New in phpvms 8
Admin panel rebuilt on Filament 5
The admin panel is now a Filament 5 resource, page, or widget served at
/admin.
The rewrite added better import/export, bulk-add modals, drag-and-drop uploads, a dashboard, a backup page (Backups), and the log viewer in the admin sidebar. You can also upload your own logo and customize the admin panel's branding.
The pilot-facing site and the theme system are largely untouched (see Themes).
Addon Registry
This version now includes an addon registry to see what the community has to offer! Installs and updates can be done directly from the manager
You'll need to make sure that the Laravel storage links are there
OAuth2 API authentication
The API can now be authenticated with OAuth2 (Laravel Passport) in addition to the legacy per-user API key:
- personal access tokens issued per user, managed from a new "API connections" page in the pilot profile;
- a grant type that exchanges an existing API key for an OAuth access token;
- permission-backed API scopes - every protected endpoint now requires a scope
such as
pireps:writeorflights:read; - short-lived access tokens with rotating refresh tokens.
- Admins can create OAuth applications to use phpvms as the authority/account source
Legacy API keys keep working unchanged. api.auth tries a Passport bearer
token first, then falls back to the old api_key lookup. The endpoint surface
is otherwise the same as 7.x (one new endpoint, one route-parameter rename; see
API changes). See API Authentication.
For ACARS v3, we recommend switching to OAuth. It unlocks new features like the Customization and Dispatcher panels.
Roles and permissions rebuilt
The old laratrust package was replaced with spatie/laravel-permission. Roles
and permissions are seeded on upgrade, there's a super-admin gate, and the admin
panel has a permission matrix for editing them.
Permission names changed. 7.x checks like ability:admin,airlines are now
granular view: / edit: / delete: permissions such as view:airlines or
view:modules (see Roles and permissions under
Breaking changes).
Pilot ID ranges, gap fill, and re-use
You can now control how the number in VMS0001 gets assigned. Five new
settings, all off by default, so an existing install keeps handing out max+1
exactly as it did in 7.x:
| Setting | Effect |
|---|---|
pilots.id_range_enabled | Confine new registrations to a numbered range |
pilots.id_range_start / pilots.id_range_end | The bounds of that range |
pilots.id_fill_gaps | Assign the lowest free ID instead of max+1 |
pilots.id_reuse_deleted | Let a soft-deleted user's ID be handed out again |
A nightly cron warns super-admins when a range fills up, at 80%, 90%, 95%, and 100% utilization. The warning goes out by mail and as an admin bell notification, the first use of Filament database notifications in phpvms. Each threshold fires once and re-arms if utilization drops back below it.
When the range is exhausted, registration throws PilotIdRangeExhausted.
UserService::createUser() now runs inside a transaction, so a failed
registration leaves no orphan row. In the admin user form, an out-of-range pilot
ID shows a warning hint rather than a validation error, so you can still assign
one deliberately.
Live map on a positions table
The live map now reads a dedicated pirep_positions table, one row per flight,
instead of resolving the newest ACARS breadcrumb for every flight on every poll.
Behavior changes you will notice:
- prefiled flights appear on the map (stationary) before they send their first position;
- completed and paused flights stay visible for a configurable period;
- editing a PIREP no longer keeps a dead flight on the map.
Map display settings moved out of the ACARS group into a new Live map
settings group, and acars.live_time was split into two settings with different
units (see Settings renames).
Flights and aircraft are archived with the PIREP
Filing a PIREP now archives its flight and aircraft onto the PIREP itself. A flight or aircraft can be retired or reused later without changing the historical PIREPs that referenced it.
Route bundles and flight visibility
Flights now belong to a flight bundle. Every existing flight is backfilled into a seeded "Default" bundle on upgrade. Bundles can carry their own start/end dates, which override the flight's own dates for visibility purposes, and can own subfleets that their flights inherit.
This came with a visibility overhaul:
flights.activewas renamed toflights.enabled;flights.visibleis now computed nightly (and after bundle edits) from whether the flight is enabled, whether its bundle is enabled, and the effective date window. It can no longer be set from the admin, and admin code must not write to it directly.
RouteForge
RouteForge, the mesh route generator, is new in phpvms 8!
This helps to generate flights and schedules all from the airports you have. You can specify the parameters, and it will generate them into new bundles. See the docs about RouteForge here.
Subfleet operational capabilities
Subfleets gained cruise_speed, max_range_nm, and route_types columns.
RouteForge uses these to decide which routes each subfleet can fly, if they're
filled in.
Typed PIREP fields
ACARS can now declare a type for each custom PIREP field value, which makes
conversions on the reading side easier. pirep_field_values gained two columns:
type- one ofNUMBER,TEXT,TIMESTAMP, orBOOLEAN. Null means an untyped legacy value, treated as text.units- an optional AIXM unit code, e.g.FTorKT.
PirepFieldValue casts the raw string value into a typed_value based on the
declared type:
// type = NUMBER
$field->value; // "1500" (raw string)
$field->typed_value; // 1500.0 (float)
// type = TIMESTAMP -> Carbon instance
// type = BOOLEAN -> bool via filter_var()
// type = TEXT / null -> raw string, unchangedNew ACARS telemetry columns
ACARS records much more of the sim state now. New columns on the acars table:
- Engine averages
eng_n1_avg_pcteng_n2_avg_pct.
- Attitude and state:
heading_magpitch,bankg_forcethrottle_pctflapson_groundgear_up.
- Exterior lights
beacon_lightsnav_lightsstrobe_lights,landing_lightslogo_lightstaxi_lightswing_lights.
- Flight phase: a new
phasecolumn, withstatuswidened to match.- Phase follows the ADREP statuses
acars.distance was widened to double. Omitted altitude and vertical-speed
readings are stored as NULL instead of 0, and PIREPs record sim_type. Logs
and events no longer live in this table at all, see
ACARS logs and events moved to pirep_events
under Breaking changes.
New Flights columns
Flights gained departure_time / arrival_time TIME columns, backfilled from
the legacy dpt_time / arr_time strings. The admin batch flight composer page
(see RouteForge) uses them when building routes in bulk.
Automatic fare pricing
Fares can now price themselves instead of using a fixed price. The price is computed from the flight distance, fare category, and airline. New auto-price columns on fares, subfleets, and airlines drive it, with an admin UI to configure them.
Reports page
phpvms 8 adds a reports page in the admin.
FrankenPHP and Laravel Octane
The production Docker stack is now FrankenPHP with a Laravel Octane runtime
(compose.deploy.yml, Dockerfile.prod), and laravel/octane is a production
dependency. On multi-node or ephemeral-filesystem deployments, set
PASSPORT_PRIVATE_KEY / PASSPORT_PUBLIC_KEY in .env so every node shares
the same signing keys.
Foundation: Laravel 13, PHP 8.4, Vite
- Laravel 10 → 13 (framework majors 11/12/13 in between).
- PHP
>=8.4.1(was>=8.1). webpack.mix.jsremoved - front-end assets build with Vite, and the toolchain is bun (bun run build).- PHPUnit replaced by Pest 4; Larastan, Rector, and Pint wired into the dev
stack;
composer testruns the full check suite.
Smaller changes
- SimBrief: airframe selection is a searchable dropdown, and an empty takeoff/landing runway (TLR) in the flight plan no longer errors.
- Airports: bulk-add modal and Enter-to-lookup on the ICAO field.
- Airlines: drag-and-drop logo upload.
- Flights: faster briefings (
with=bidfast path), and bundle-inherited subfleets are capped per list page with a report when the cap is hit. - Import/export: aircraft import/export re-added; subfleet CSV import/export now includes type ratings.
- ACARS: incoming log/event strings are capped at 1000 characters.
- Notifications: Discord announcements now go through
laravel-discord-notifier, which also supports posting through a bot (channel ID +DISCORD_BOT_TOKEN) instead of a webhook URL. - Admin ergonomics: form actions right-aligned with cancel-before-save, per-module Filament panels with a panel switcher, relative URLs accepted, log-viewer assets published on composer update.
- API: the user resource now includes role names and effective permissions, and the OAuth token response includes them too.
- Dev tooling: ide-helper regenerated on install, Docker images tagged with major-minor versions.
Breaking changes
Requirements
- PHP 8.4.1+ (was 8.1+). Extension requirements are unchanged (json, mbstring, simplexml, bcmath, pdo, intl, zip).
- Laravel 13: anything pinned to Laravel 10 internals must be re-checked.
App bootstrap now lives in
bootstrap/app.php- there's noapp/Http/Kernel.phpor middleware-groups config file anymore. - Front-end builds: custom theme/front-end builds must be ported from webpack/mix to Vite + bun.
.env variable renames. phpvms 8 only reads the new names; old ones are
silently ignored:
| 7.x name | 8.0 name |
|---|---|
QUEUE_DRIVER | QUEUE_CONNECTION |
CACHE_DRIVER | CACHE_STORE |
MAIL_DRIVER | MAIL_MAILER |
BROADCAST_DRIVER | BROADCAST_CONNECTION |
QUEUE_WORKER | RUN_QUEUED_JOBS_IN_CRON |
Defaults also changed when no env var is set: cache array → database, queue
sync → database, session file → database, database mysql → sqlite,
mailer smtp → log. Bring your old values across under the new names.
New env vars: APP_LOCALE, TRUSTED_PROXIES (default *),
PASSPORT_PRIVATE_KEY / PASSPORT_PUBLIC_KEY, OCTANE_HTTPS, VITE_APP_NAME,
MAIL_SCHEME.
Application code
These matter mainly to developers, but they explain the "rewrite" character of this release:
- Repositories removed.
prettus/l5-repositorywas dropped along withapp/Repositoriesandconfig/repository.php. Search/read paths now use dedicated Query classes (AirportSearchQuery,UserSearchQuery,PirepSearchQuery) and model scopes. - Enums moved and converted to native PHP enums.
App\Models\Enums\*→App\Enums\*, and the class-based enum objects became native backed enums.App\Models\Casts\*,App\Models\Observers\*, andApp\Models\Traits\*moved toApp\Casts\*,App\Observers\*, andApp\Traits\*.App\Models\Enums\PirepStatussurvives as aclass_aliasofApp\Enums\PirepPhase(deprecated). - Removed models and services:
Module,SimBriefXML,AirportLookup,AnalyticsService,DatabaseService. (GeoJsonmoved toApp\Support.)
Addons and modules
- The module system was replaced:
nwidart/laravel-modulesandjoshbrw/laravel-module-installerare gone. Addons are installed asphpvms-modulecomposer packages, registered through the addon service provider from theaddonstable + boot cache, with a validatedmodule.jsonmanifest (name, alias, providers, type, compat, registry_id, version). - The bundled
SampleandVacentralmodules are deleted. - VaCentral integration removed: the
nabeel/vacentralpackage and its config keys are gone. Airport data lookups now come from the phpvms API (api.phpvms.net) instead.
A dedicated guide for updating addons is in progress.
Roles and permissions
laratrust → spatie/laravel-permission brings new tables, new APIs
(config/permission.php, config/roles.php), and granular permission names
(view: / edit: / delete: + subject, plus access:<module> for module
panels). Roles and permissions are seeded by a data migration, so the default
roles keep working.
What to do: any custom code that called the laratrust API (roles(), permissions checks, ability middleware) must be rewritten for spatie.
Database and migrations
- Migrations moved from
app/Databasetodatabase/migrationswith data migrations indatabase/migrations_data. Historical migrations must not be edited; 8.0 ships data migrations instead. - Destructive on upgrade:
add_acars_pirep_foreign_keydeletes orphanedacarsrows in batches before adding a foreign key. These rows were left behind by 7.x PIREP hard-deletes and were already unreachable, but the deletion is permanent, so back up first. On SQLite the constraint is skipped (the purge and column widening still apply). - Required action:
php artisan flights:migrate-time-columnsparses legacydpt_time/arr_timestrings into the new TIME columns. Idempotent. - Renames:
flights.active→flights.enabled(raw SQL against the old name fails);Flight::active()is a deprecated alias ofFlight::visible();Pirep::position()now returns aPirepPosition;Pirep::scopeActiveFlights()is gone;FlightResourceemitsactiveonly as a deprecated alias ofenabled. flights.visibleis cron-managed - do not write to it from admin code.- The
users.pilot_idunique index is now a plain index. ID re-use means an active user and a soft-deleted one can share a number, so the database no longer enforces uniqueness.UserServiceenforces it instead, under a cache lock. Addon code that relied on the constraint to catch duplicate writes needs to check for itself.
ACARS logs and events moved to pirep_events
In 7.x, ACARS events and logs were LOG rows in the acars table whose log
column held either a plain string from the legacy endpoints or a JSON blob from
the plugin's TelemetryWriter. None of it was queryable, so phase detection had
to regex-scan the text. Both now go to a dedicated pirep_events table with
typed type, category, and phase columns plus a JSON details bag.
acarsLOG rows are no longer written or read.POST /acars/logsandPOST /acars/eventsclassify each incoming string and write an event. There is no dual-write period, because every reader moved in the same change: Filament'sViewPirep, the seven theme PIREP view, andPerformanceChartServicephase detection.- Required action:
php artisan phpvms:backfill-pirep-eventsmigrates existing history. It is idempotent, and--deletepurges the migratedacarsrows afterward. Until you run it, old PIREPs show no log entries. That gap is the intended upgrade window, not a bug. pirep_events.acars_idusesON DELETE RESTRICT, the first foreign key between two domain tables in the schema. Telemetry rows referenced by an event cannot be deleted, which makes the invariant a database rule rather than a convention. Both paths that bulk-delete a PIREP'sacarsrows now delete itspirep_eventsfirst. Addon code that deletesacarsrows directly must do the same or it will fail with an FK error.EventClassifieris a direct port of the client'sLegacyEventClassifier.cs, andtypeholds the same kebab-case wire slugs the plugin sends, so legacy-classified and plugin-written rows share one vocabulary. Anything it cannot match degrades totype = null,category = "message", with the raw string preserved.
Settings renames
The settings table and the setting() helper are unchanged, but some keys moved
or were renamed. Renames carry your configured values across via data migration:
| Old key | New key | Notes |
|---|---|---|
acars.live_time | pireps.tombstone_time | hours; governs silent-PIREP cancellation |
acars.center_coords, acars.default_zoom, acars.update_interval | livemap.* | map display settings |
| — | livemap.live_time (minutes), livemap.idle_time (minutes) | how long finished/paused flights stay drawn |
notifications.discord_public_webhook_url | notifications.discord_public_route | accepts a webhook URL or a bot channel ID |
notifications.discord_private_webhook_url | notifications.discord_private_route | same |
New settings: fares.auto_price, fares.low_cost_multiplier,
pilots.only_show_flights_from_current, pilots.id_range_enabled,
pilots.id_range_start, pilots.id_range_end, pilots.id_fill_gaps,
pilots.id_reuse_deleted, registry.public_key, va_global_id.
Removed config files
config/broadcasting.phpconfig/compile.phpconfig/cron.phpconfig/flare.phpconfig/gravatar.phpconfig/ignition.phpconfig/importer.phpconfig/installer.phpconfig/languages.phpconfig/laratrust.phpconfig/map.phpconfig/repository.phpconfig/self-update.phpconfig/updater.phpconfig/vacentral.phpconfig/view.php
Most were empty or near-empty; the contents that mattered moved elsewhere: the
languages list and METAR-WMS URL moved into config/phpvms.php, laratrust.php
became permission.php + roles.php, and the importer batch_size setting was
dropped.
What to do: if you customized any of these files, check whether your change
still applies in its new home (or .env).
Stale files on an in-place upgrade
Composer upgrades remove these for you. In-place upgrades leave them on disk, so delete anything below that no longer ships in 8.0. Dead files rarely break a running app, but clearing them avoids confusion.
Directories removed or relocated:
app/Database/- migrations moved todatabase/migrations/app/Repositories/- repository pattern dropped for query classesapp/Models/Enums/,app/Models/Casts/,app/Models/Observers/,app/Models/Traits/- moved toapp/Enums/,app/Casts/,app/Observers/,app/Traits/modules/Sample/,modules/Vacentral/- bundled modules deletedresources/sass/- front-end assets build through Vite nowresources/views/layouts/beta/,resources/views/layouts/default/- removed themes
Files removed:
webpack.mix.js- replaced by Viteapp/Http/Kernel.php- bootstrap moved tobootstrap/app.phpapp/Models/Module.php,app/Models/SimBriefXML.phpapp/Services/AnalyticsService.php,app/Services/DatabaseService.php,app/Services/AirportLookup/- the removed config files listed above
This is the notable set, not the full diff - the 7.x-to-8.0 rewrite removes around 800 files. The upgrade guide will carry the complete manifest.
Themes
The theme system (igaster/laravel-theme) is unchanged. Themes live in
resources/views/layouts/<name>, and the default is seven (via
DEFAULT_THEME or the general.theme setting).
The beta and default themes were removed, so seven is the only shipped
theme now. The layout itself is largely the same, but it loads assets through
Vite and gained views for the OAuth authorize page, the profile API connections
page, and the SimBrief username page.
What to do: custom themes need their asset pipeline ported to Vite. If your
theme derived from beta or default, base it on seven.
A new SPA theme built on Vue is on the way.
Behavior changes to watch for
- Live map: prefiled flights are visible before they move; completed/paused
flights linger per the new
livemap.*settings; editing a PIREP no longer keeps a dead flight on the map. flights.days(day-of-week scheduling) is no longer honored by the visibility cron. The field still exists and is editable, but in 7.xSetActiveFlightshid flights on non-scheduled days and nothing does that now. Day-scheduled flights are now visible every day unless a bundle window says otherwise. Audit any use offlights.daysafter upgrading.- ACARS log entries are capped at 1000 characters.
- PIREP custom fields can be typed. ACARS writes that declare a
type(NUMBER/TEXT/TIMESTAMP/BOOLEAN) get typed values; rows without a type keep plain-string behavior.
API changes
The REST API surface is unchanged from 7.x except:
GET /api/airports/{id}becameGET /api/airports/{airport}(route-model binding; the response is identical).POST /api/users/simbrief_usernameis new (scopesettings:write).- Every authenticated route enforces an OAuth scope via middleware. The nine
scopes:
airlines:read,airports:read,bids:write,fleet:read,flights:read,pireps:read,pireps:write,settings:write,user:read. Legacy per-user API keys hold a wildcard scope, so they satisfy every check and keep working unchanged.
See API Authentication.