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:

  1. PHP 8.4.1 or newer is required - 7.x ran on PHP 8.1+. See Requirements.
  2. Take a database backup. One migration permanently deletes orphaned acars rows (see Database and migrations).
  3. 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:write or flights: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:

SettingEffect
pilots.id_range_enabledConfine new registrations to a numbered range
pilots.id_range_start / pilots.id_range_endThe bounds of that range
pilots.id_fill_gapsAssign the lowest free ID instead of max+1
pilots.id_reuse_deletedLet 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.active was renamed to flights.enabled;
  • flights.visible is 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 of NUMBER, TEXT, TIMESTAMP, or BOOLEAN. Null means an untyped legacy value, treated as text.
  • units - an optional AIXM unit code, e.g. FT or KT.

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, unchanged

New ACARS telemetry columns

ACARS records much more of the sim state now. New columns on the acars table:

  • Engine averages
    • eng_n1_avg_pct
    • eng_n2_avg_pct.
  • Attitude and state:
    • heading_mag
    • pitch, bank
    • g_force
    • throttle_pct
    • flaps
    • on_ground
    • gear_up.
  • Exterior lights
    • beacon_lights
    • nav_lights
    • strobe_lights,
    • landing_lights
    • logo_lights
    • taxi_lights
    • wing_lights.
  • Flight phase: a new phase column, with status widened 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.js removed - 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 test runs 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=bid fast 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 no app/Http/Kernel.php or 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 name8.0 name
QUEUE_DRIVERQUEUE_CONNECTION
CACHE_DRIVERCACHE_STORE
MAIL_DRIVERMAIL_MAILER
BROADCAST_DRIVERBROADCAST_CONNECTION
QUEUE_WORKERRUN_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-repository was dropped along with app/Repositories and config/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\*, and App\Models\Traits\* moved to App\Casts\*, App\Observers\*, and App\Traits\*. App\Models\Enums\PirepStatus survives as a class_alias of App\Enums\PirepPhase (deprecated).
  • Removed models and services: Module, SimBriefXML, AirportLookup, AnalyticsService, DatabaseService. (GeoJson moved to App\Support.)

Addons and modules

  • The module system was replaced: nwidart/laravel-modules and joshbrw/laravel-module-installer are gone. Addons are installed as phpvms-module composer packages, registered through the addon service provider from the addons table + boot cache, with a validated module.json manifest (name, alias, providers, type, compat, registry_id, version).
  • The bundled Sample and Vacentral modules are deleted.
  • VaCentral integration removed: the nabeel/vacentral package 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/Database to database/migrations with data migrations in database/migrations_data. Historical migrations must not be edited; 8.0 ships data migrations instead.
  • Destructive on upgrade: add_acars_pirep_foreign_key deletes orphaned acars rows 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-columns parses legacy dpt_time / arr_time strings into the new TIME columns. Idempotent.
  • Renames: flights.active → flights.enabled (raw SQL against the old name fails); Flight::active() is a deprecated alias of Flight::visible(); Pirep::position() now returns a PirepPosition; Pirep::scopeActiveFlights() is gone; FlightResource emits active only as a deprecated alias of enabled.
  • flights.visible is cron-managed - do not write to it from admin code.
  • The users.pilot_id unique 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. UserService enforces 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.

  • acars LOG rows are no longer written or read. POST /acars/logs and POST /acars/events classify each incoming string and write an event. There is no dual-write period, because every reader moved in the same change: Filament's ViewPirep, the seven theme PIREP view, and PerformanceChartService phase detection.
  • Required action: php artisan phpvms:backfill-pirep-events migrates existing history. It is idempotent, and --delete purges the migrated acars rows afterward. Until you run it, old PIREPs show no log entries. That gap is the intended upgrade window, not a bug.
  • pirep_events.acars_id uses ON 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's acars rows now delete its pirep_events first. Addon code that deletes acars rows directly must do the same or it will fail with an FK error.
  • EventClassifier is a direct port of the client's LegacyEventClassifier.cs, and type holds the same kebab-case wire slugs the plugin sends, so legacy-classified and plugin-written rows share one vocabulary. Anything it cannot match degrades to type = 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 keyNew keyNotes
acars.live_timepireps.tombstone_timehours; governs silent-PIREP cancellation
acars.center_coords, acars.default_zoom, acars.update_intervallivemap.*map display settings
—livemap.live_time (minutes), livemap.idle_time (minutes)how long finished/paused flights stay drawn
notifications.discord_public_webhook_urlnotifications.discord_public_routeaccepts a webhook URL or a bot channel ID
notifications.discord_private_webhook_urlnotifications.discord_private_routesame

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.php
  • config/compile.php
  • config/cron.php
  • config/flare.php
  • config/gravatar.php
  • config/ignition.php
  • config/importer.php
  • config/installer.php
  • config/languages.php
  • config/laratrust.php
  • config/map.php
  • config/repository.php
  • config/self-update.php
  • config/updater.php
  • config/vacentral.php
  • config/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 to database/migrations/
  • app/Repositories/ - repository pattern dropped for query classes
  • app/Models/Enums/, app/Models/Casts/, app/Models/Observers/, app/Models/Traits/ - moved to app/Enums/, app/Casts/, app/Observers/, app/Traits/
  • modules/Sample/, modules/Vacentral/ - bundled modules deleted
  • resources/sass/ - front-end assets build through Vite now
  • resources/views/layouts/beta/, resources/views/layouts/default/ - removed themes

Files removed:

  • webpack.mix.js - replaced by Vite
  • app/Http/Kernel.php - bootstrap moved to bootstrap/app.php
  • app/Models/Module.php, app/Models/SimBriefXML.php
  • app/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.x SetActiveFlights hid 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 of flights.days after 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} became GET /api/airports/{airport} (route-model binding; the response is identical).
  • POST /api/users/simbrief_username is new (scope settings: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.

On this page