Changelog
Every change, in order.
Features, fixes and breaking changes for each release, so an upgrade holds no surprises.
v0.1.0
latestA WebSocket server speaking the Pusher protocol, so Laravel Echo connects to it unchanged.
-
New
The Pusher wire protocol: connection_established, subscribe and unsubscribe, ping and pong, subscription_succeeded, member_added and member_removed, and
client-*events. Laravel Echo, pusher-js and pusher/pusher-php-server all work against it without a shim, which is the entire reason for implementing the protocol rather than inventing one -
Breaking
Built on ReactPHP, not Workerman. Laravel Reverb requires react/socket and ratchet/rfc6455 and no Workerman at all, so this uses the same stack. The previous bespoke protocol and its
#[WsRoute]attribute API are gone — nothing that spoke to the old server speaks to this one -
New
Public, private and presence channels, told apart by the name prefix. The prefix is the whole rule, so a channel cannot be left unprotected by forgetting to declare it: naming it
private-is the declaration - New A presence roster that counts people rather than connections. One user with two tabs is one member, and closing one tab is not leaving — which is the bug every presence implementation has first, and the one users notice
- Security Signatures cover the socket id, so one minted for a connection cannot be replayed by another. Presence channel_data is verified byte-for-byte as sent rather than re-encoded: re-encoding produces different JSON, so correct signatures start failing, and the natural fix for that is to stop checking
-
Security
The publishing API is signed with Pusher's own scheme. The signature covers the request body, so a captured publish cannot be edited and replayed, and carries a timestamp, so it cannot be replayed at all after ten minutes.
pusher:andpusher_internal:names are reserved, since a forged member_added would corrupt every roster listening - Security A private or presence channel with no rule in routes/channels.php is refused. The alternative allows what nobody has written a rule for, which makes every private channel public until somebody remembers it exists
- New A heartbeat. Connections that vanish without a FIN — a laptop lid closing, a phone changing network — are pinged and dropped, rather than staying in memory as members of every presence channel they joined
v0.11.2
latestBroadcasting, which had never worked, now does.
-
Fix
broadcast(new SomethingHappened)was a fatal error. The helper calledBroadcastManager::send(), a method that has never existed on that class, so every documented use of it raised "Call to undefined method" — which means nothing has ever been broadcast through it -
New
BroadcastManager::extend()lets a package register a broadcaster. A driver had to be acreate<Name>Drivermethod on the manager, so the only way to add one was to edit the framework, which made broadcasting the single subsystem a package could not extend and a realtime package the obvious thing that could not be written - Improvement The unknown-driver exception lists what is available. "Driver [x] is not supported" without naming the alternatives turns a typo into a hunt through the framework
v0.6.0
latestRoles and permissions are enforced. Until now they were furniture.
- Security The admin JSON API had no authentication on any route. Thirteen routes, no middleware, so every one answered anybody who asked, from anywhere, with no session — including the audit trail, which holds the full contents of deleted records
-
Security
POST /admin/api/loginaccepted any email and password without checking either, and replied{"token": "your-token-here"}. A client built against it believes it has authenticated somebody - Security The roles, permissions, role_user and permission_role tables shipped from the first release and nothing ever read them. Access control was authentication-only: any account that could log in could create, edit and delete every record of every registered resource. Every resource action, the media library and the audit trail check a permission now
- Security An unknown answer is a denial: missing tables or a failed query means no, not yes. Failing open so as not to lock anyone out turns a broken migration into an unprotected panel, silently. An admin with no roles can do nothing, rather than everything
-
New
admin:sync-permissionscreates the four default roles, writes the permissions each resource implies, and grants each role what it is defined as having. Re-running only adds, so a permission revoked by hand stays revoked -
Fix
admin:assign-roleandadmin:revoke-rolereported success and wrote nothing — so someone removing access from a departing colleague saw it confirmed and it had not happened. Revoking the last superadmin now needs--force - Removed The stubbed JSON API: login, logout, me and the eight resource methods, every one a TODO returning a fabricated success. A token-authenticated CRUD API is a feature to design, not a hole to patch
-
Breaking
Existing admin accounts hold no roles, so after upgrading they can sign in and do nothing. Run
admin:sync-permissions, thenadmin:assign-role <email> superadmin. The sync command detects this and prints the exact command, naming your accounts — it does not fix it for you, because granting superadmin to whichever account happens to be first is a privilege escalation performed by a migration
v0.5.0
stableThe audit trail records something. Until now it recorded nothing at all.
-
New
audit_logsshipped from the first release, with two API endpoints to read it, and nothing ever wrote a row. The panel could create, edit and delete any record in the application and leave nothing behind saying who did it. Logins, failed logins, logouts, and every resource write are recorded now, with the acting admin, the resource and id, the IP and user agent, and the values - Security Old values are read before the write, not after. Snapshotting afterwards records the new values twice and loses the only copy of what was there before — and for a delete, the only copy anywhere
-
Security
password,remember_token,api_tokenandsecretare stored as[redacted]. An audit row is read by more people than the record it came from - Security Failed logins are recorded with the attempted address and no actor. That half is the more interesting one: a run of them against a single address is what a brute-force attempt looks like from inside the panel
- Improvement Recording never throws. A trail that can take a request down with it is one that gets switched off by the first person it inconveniences
-
New
GET /admin/api/audit-logsreads the table for real, filterable by event, resource type and admin, paginated and capped at 100 per page, with the value columns decoded rather than handed back as JSON inside JSON
v0.4.0
stableThe panel could not be logged into. Once that was fixed, nothing could be saved.
- Security No form in the panel carried a CSRF token — login, create, edit, delete and logout all posted without one. With CSRF middleware on, which is the default, every write answered 419 and the panel could not be signed into at all. A test now asserts one token per form across every shipped view
-
Fix
admin:make-resourcegenerated a class that fataled the moment it was autoloaded: it declaredstatic string $modelagainst the base class's?string, and PHP requires a redeclared typed property to match exactly. The command reported success and the file looked correct -
Fix
admin:make-userignored its password argument, so the documented non-interactive form hung; with--no-interactionthe prompt returned null andpassword_hash()threw a type error. It also claimed to assign--rolewhile writing nothing -
Fix
The resource table rendered every column as plain text, so badges, booleans and images came out as raw values and their view partials were dead code.
formatUsing()was stored and never applied, which meantdateTime()did nothing -
Security
The detail page cast the record to an array and printed every key, so a column deliberately left out of
columns()was still shown in full one click away. The allow-list holds on every page that renders a record now -
Fix
Records created through the panel had empty timestamps, so anything sorting on
created_at— the default resource sort included — treated them as the oldest rows in the table - Security A constraint violation rendered the driver's message at 500, naming the table and column. Create and edit ignored flashed errors entirely, so a failed save redirected back to a form that looked untouched
-
Removed
The settings routes.
index()rendered a view that was never written, so the page answered 500, andupdate()returned "Settings updated successfully" without writing anything anywhere
v0.11.1
stableFour silent failures, all found by building an application on the framework rather than reading it.
-
New
Router::fallback(), a catch-all matched after every other route regardless of when it was registered. A wildcard at the bottom ofroutes/web.phpis matched in registration order, and packages register their routes later, while their providers boot — so the wildcard swallowed every route a package added. Installing an admin panel 404'd the whole panel while both files looked correct -
Fix
Model::where($column, $value)threw "Unsupported SQL operator". Three arguments were forwarded unconditionally, and the builder decides operator-or-value by counting them, so the value was read as an operator. The most common query anyone writes against a model did not work -
Fix
The HTTP kernel is a shared instance.
pushMiddleware()is documented as how packages register global middleware, but the kernel was unbound, so resolving it built a fresh object and the push landed on a kernel no request passed through. The middleware simply never ran -
Fix
Published views actually override the package's now.
vendor:publishwrote a full copy of every view that the engine then ignored, because only the package's own directory was registered. Editing one had no effect and nothing said why
v0.1.1
latestAccepts framework 0.11.
-
Improvement
The constraint was
^0.10.x, which below 1.0 excludes 0.11 entirely, so the package could not be installed alongside the framework release it was built against. No code changed: the tests pass unaltered
v0.1.1
latestAccepts framework 0.11.
-
Improvement
Same constraint widening as Secure. Below 1.0 Composer treats the minor as the compatibility boundary, so
^0.10.0will not resolve against 0.11
v0.1.0
stableEncryption you can rotate, an audit trail, and threat detection.
- New More than one encryption key alive at once. Values carry the id of the key that wrote them, so an old key keeps decrypting what it wrote and rotating becomes two steps that can be weeks apart, rather than an event that makes everything unreadable
- Security AES-256-GCM with the key id authenticated, so an altered payload is refused rather than decrypted into something plausible, and a payload cannot be pointed at a different key
-
New
An audit trail recording who did what, to which record, and what it looked like before.
recordChangestores only the fields that differ - Security Anything whose field name looks sensitive is replaced before it is written, recursively and case-insensitively, so a password nested inside a request payload is caught too
- New Threat detection counting events per source against thresholds in a sliding window, with a middleware answering 429 for a blocked source
- Improvement Audit writes never throw: a trail that can end the request turns a logging problem into an outage, under exactly the load where the trail matters most
-
New
secure:key,secure:statusandsecure:prune. Nothing prunes automatically
v0.1.0
stableA profiler, a dumper and generators for the classes the framework has no maker for.
-
New
A profiler recording spans you open explicitly, reported through a
Server-Timingheader that browsers display natively. That works for JSON, redirects and downloads, and it cannot alter the response body -
Improvement
Timings come from
hrtime, notmicrotime: the system clock can move backwards, and a negative duration sends people hunting a bug in their own code -
New
A dumper that keeps types visible, reports string lengths, prints whole floats as
1.0, showsNANandINF, and includes private properties with readable names -
New
Generators for services, actions, DTOs and repositories, including subdirectories. DTOs come out
final readonly; actions get one method - Improvement Profiling follows the environment when unconfigured: on in local and testing, off everywhere else, so doing nothing gives the safe answer
v0.11.0
stableThe Nova admin module is removed. It never worked.
-
Removed
The Nova admin module. Its controllers took
$resourceKeywhile its routes declared{resource}, so the container could not resolve the parameter and every Nova route raised an error rather than rendering anything -
Breaking
Libxa\Nova\*andNovaServiceProviderno longer exist. Removing public classes is why this is a minor bump rather than a patch -
Fix
Nova claimed the
/adminprefix among the core providers, so it collided with any admin package a project installed and whichever registered first silently won. That collision is how the broken routes were finally noticed
v0.10.3
stableThe column types a real schema needs.
-
New
Blueprint::unsignedBigInteger(), the correct type for a foreign key referencingid().bigInteger()is signed andunsignedInteger()is too narrow, so there was no right answer before -
New
Blueprint::ipAddress(), 45 characters, which is the longest an IPv6 address gets. The alternative people reach for isstring(15), which holds every IPv4 address and silently truncates every IPv6 one -
New
Blueprint::primary()for composite primary keys, emitted insideCREATE TABLEbecause SQLite cannot add one withALTER TABLEafterwards
v0.10.2
stableA package could not ship a migration, and nothing said so.
-
Fix
ServiceProvider::loadMigrationsFrom()was guarded by a check for amigratorbinding that nothing ever created, so the method silently did nothing and any package following the documented API shipped a migration that could never run -
Fix
migrateconstructed its ownMigrator, discarding whatever a service provider had registered during boot -
Fix
Migrator::addPath()ignores a path it already holds. With several places contributing paths, a duplicate ran every migration in it twice
v1.0.0
latestOne command to create an application: libxa new my-app.
-
New
libxa new my-apprunscomposer create-projectand everything around it: the database chosen up front, front-end dependencies installed, a first commit, and a summary of what to run next -
New
SQLite, MySQL, MariaDB or PostgreSQL, asked for interactively or given with
--database -
New
--git,--branch,--github[=visibility],--organization,--npm,--devand--force - New One-line install scripts for Windows, macOS and Linux. Both install per-user and add the command to PATH, with no administrator rights
-
Improvement
Choosing a database rewrites only
DB_DRIVER,DB_PORTandDB_DATABASE, so theAPP_KEYthe skeleton generates survives - Improvement Shipped as a self-contained executable, so the installer needs nothing installed to run
- Improvement Every prompt has a non-interactive answer, so the same commands run in CI rather than hanging on a question nothing can answer
v0.4.0
latestRouting works on a deployed server, which it had never done.
-
Fix
Every route except the home page returned 404 once deployed. The kit shipped no
.htaccessat all, so Apache looked for a file namedloginwhen asked for/loginand returned its own 404 before PHP started -
New
src/public/.htaccess: the front controller rule,MultiViewsoff so Apache cannot resolve/abouttoabout.phpand bypass routing, directory indexes off, and one canonical URL per page -
Security
The
Authorizationheader is restored after CGI and FastCGI strip it. Without it, API token authentication fails with nothing to say why -
New
A root
.htaccessfor shared hosting, where the document root is the project directory and cannot be moved: it rewrites intosrc/public/and refusesvendor/,src/app/,src/storage/and.env -
New
DEPLOYMENT.md, shipped inside the generated project, with working configuration for Apache, nginx, Caddy and shared hosting -
Improvement
DeploymentConfigTestfails in CI if the rewrite rules stop shipping, because the original bug was these files not existing
v0.4.0
latestA rebuilt Sites page, and three settings that saved correctly and then changed nothing.
- New The Sites page is a list beside a detail pane. The table had six controls and a full path in every row, so each per-site setting made the others narrower
- New Preview renders in the page rather than in its own window, at a desktop, tablet or mobile width: looking at a site while changing its settings used to mean a window covering the settings
- New A Node version per site, or Default to follow the global one
- New Startup settings: launch at login, and start minimised as a tray application
- New A fuller tray menu: quick access to sites, per-service start and stop, switching the default PHP version, and the configuration directory
-
Fix
Changing a site’s PHP version did nothing. The setting saved, so the dropdown looked right, while nginx kept serving the old config and no
php-cgiwas ever started for the version chosen - Fix Switching the default PHP version had the same gap, and it decides which binary the service runs, so nothing took effect until the next launch
- Fix A version’s FastCGI port no longer moves when another is added or removed. It used to be an index into a sorted list, so one change shifted every port after it and broke sites nobody had touched
-
Fix
Auto-update could never find a release: the updater asked for
draftreleases, which the unauthenticated API does not return - Fix A terminal that launched and immediately died counted as success, ending the search with no window and no message
- Removed The separate preview window, replaced by the one in the page
v0.3.0
stableHTTPS for local sites, a preview window, and a terminal at any project.
-
New
HTTPS for local sites. A per-machine certificate authority signs a certificate for any site with TLS switched on, covering the domain, its wildcard,
localhostand127.0.0.1 - New One button trusts the root certificate. It does not prompt for elevation because the per-user store does not need any
- Fix The per-site TLS toggle now does something. It had been in the data model and the IPC surface since 0.1.0, but nginx ignored it: a site marked secure was served over plain HTTP with nothing to indicate it
- New Site preview: one window per site at a desktop, tablet or mobile viewport, in its own session partition
- New Open a terminal at any project. Windows Terminal, PowerShell or cmd on Windows; Terminal on macOS; the common emulators on Linux, tried in order
- Improvement Certificates last 396 days, under the 398 Chrome will accept, and renew a month before they lapse
- Removed System information no longer lists the versions of the shell the app is built on. They describe the shell, not the environment being managed
v0.10.1
stablePresentation only. No API, behaviour or dependency changed.
-
Improvement
The built-in error pages use the standard unpunctuated HTTP status phrases:
404 Not Found,405 Method Not Allowed,500 Server Error -
Improvement
The 405 page lists the permitted methods as
Allowed: GET, POST, and titles read404 | LibxaFrame - Improvement Em dashes removed from the source, the comments and the documentation
v0.2.0
stablephpMyAdmin, and per-site PHP versions that are actually routed.
-
New
phpMyAdmin: installed from the project’s own host with a published SHA-256, configured against the managed database, and served at
phpmyadminlibxa.test - Security The phpMyAdmin vhost is bound to localhost: it administers the database as root, and nginx binds every interface
- New phpMyAdmin runs on the newest installed PHP its release supports, rather than the newest PHP present
-
Fix
Per-site PHP versions are now routed. One
php-cgiper version in use, with each server block pointing at the matching port: the version dropdown had been recorded, displayed and ignored -
Fix
Orphaned
php-cgiworkers are stopped on quit, so a relaunch does not find its port taken and relocate the environment - Breaking Any site pinned to a version other than the default was silently served by the default until now, and moves to its configured version on upgrade
v0.1.0
stableFirst release of the desktop environment.
- New Supervises PHP-FPM, nginx and MariaDB as child processes, with combined logs and a stop that does not orphan workers
- New Runtime installer: nginx, MariaDB, PHP 8.0–8.5 and Node 20–26, each verified against a publisher-supplied SHA-256 before extraction
- New Site discovery: park a directory and every servable project inside it gets a local domain and a generated server block
- New Scaffolds new LibxaFrame and Laravel applications, fetching a verified Composer when the machine has none
-
New
Writes
.testdomains into a marked block of the hosts file, leaving every other entry, including Herd’s, untouched -
New
Self-update from the public releases repository, verified against the SHA-512 in
latest.ymland installed only on request - New Custom window chrome and a tray menu, carrying the LibxaFrame mark
v0.10.0
stableStability pass across the whole framework, and PHP 8.5 support.
- New PHP 8.5 supported and tested in CI alongside 8.3 and 8.4
- Improvement 197 tests covering the container, HTTP kernel, router, validation, Atlas, sessions and encryption
-
Fix
Header name normalisation in
Request, and a safe-referer check inResponseso a redirect cannot be pointed off-site - Fix Router, route collection and pipeline corrected for middleware ordering and short-circuiting
- Fix Atlas query builder, schema and migrator hardened against the edge cases the new tests found
- Security CSRF, throttle and shared-errors middleware reviewed and corrected
- Fix Vite manifest emits each asset once: duplicate tags were being produced for entries that shared a chunk
v0.3.0
stableDependency resolution made reproducible, and PHP 8.5 in CI.
-
Fix
Stability flags are per-package rather than global, so a
devrequirement no longer loosens every other dependency -
Fix
config.platform.phppinned to 8.3: the lock file had been resolving packages that require 8.4.1 while the manifest declared^8.3 - Fix The path repository is non-canonical, so Packagist is still consulted and a published install resolves the same way a local one does
-
Improvement
30 tests, including one that fails if the lock file records a
pathdist - New PHP 8.5 added to the CI matrix
Upgrading?
The upgrade guide walks through every breaking change, in order, with the code to change.
Read the upgrade guide