6 min read

NativePHP Desktop 2.2: Nightwatch Support, and a Loopback Fix You Should Not Skip

NativePHP Desktop 2.2 adds Nightwatch integration and window polish, then 2.2.1 patches a real issue where the Electron API server could bind past localhost.

Featured image for "NativePHP Desktop 2.2: Nightwatch Support, and a Loopback Fix You Should Not Skip"

Every NativePHP Desktop app runs on a quiet piece of plumbing most developers never think about. Your Laravel application does not talk to the Electron shell through magic. It talks to it over an internal HTTP API, the bridge that lets Notification::send(), Window::open(), and every other NativePHP facade actually reach the native window sitting on top of your PHP process. That bridge has to bind to a port and listen. And for a while, depending on the host environment, it could end up listening on more than just your own machine.

That is the story behind NativePHP Desktop’s last two releases. 2.2.0 shipped a batch of genuinely useful features in late April 2026. 2.2.1 followed about a month later with a hotfix that every NativePHP Desktop developer should apply, because it closes a gap between what the API was supposed to expose and what it sometimes actually did.

What landed in 2.2.0

The headline feature in 2.2.0 is first-party support for Laravel Nightwatch, the framework’s official production monitoring service. If you have not looked at Nightwatch yet, it is Laravel’s own answer to APM: request tracing, query profiling, and job monitoring that understands Laravel’s internals rather than treating your app as a generic PHP process. Wiring it into a NativePHP Desktop app means you get the same production visibility for a shipped desktop binary that you would get for a web app behind Forge or Vapor, which matters more than it sounds like once you have a few thousand installs quietly running on other people’s machines.

A few smaller additions round out the release:

  • A new upperCenter() window positioning method, alongside the existing corner and center helpers, for placing windows consistently across platforms.
  • A --no-focus flag for the native:run Artisan command, so you can launch your app during development without it stealing window focus every time you save a file.
  • Compatibility updates for Laravel 13.x, keeping the package aligned with the framework’s current release line.

None of that is dramatic on its own. It is the kind of release that makes a tool nicer to live with day to day, which is exactly what you want from a package that is supposed to get out of your way.

The problem 2.2.1 actually fixes

2.2.1, released a month later, is a smaller release by line count but a more important one to understand. The relevant change is described plainly in pull request #114: “bind Electron API server to loopback.”

Here is the mechanism. NativePHP Desktop runs an internal Express server inside the Electron process. That server is how your PHP code and the native shell exchange commands, and it is protected by an X-NativePHP-Secret header so that only your own app can talk to it. The problem was never that the endpoint was unauthenticated. The problem was where it was listening. Node’s server.listen(port) will bind to all available network interfaces unless you explicitly tell it not to, and that is exactly what was happening here. Depending on the host machine and its network configuration, the API server could end up reachable from other devices on the same LAN, not just from the app that was supposed to own it.

This was not a theoretical concern. The fix references a real bug report from a NativePHP-based app called TimeScribe, whose maintainer noticed the app’s internal port exposed on an external network interface and traced it back to this exact behavior in the desktop package.

The fix itself is narrow and correct: bind the API server explicitly to 127.0.0.1, update the internal NATIVEPHP_API_URL to point at 127.0.0.1 instead of localhost (removing a DNS resolution step that could theoretically resolve elsewhere), and add test coverage asserting the server never binds to 0.0.0.0. The public API and the secret-header authentication are unchanged. This is purely about narrowing the network surface the server is allowed to touch.

Why this is worth your attention even if nothing “happened”

It is tempting to file this under routine maintenance and move on, especially since the header-based auth meant a stray request without the right secret would have been rejected anyway. But defense in depth exists precisely for cases like this. An internal API that is only reachable from 127.0.0.1 has an entire class of attack removed from consideration: no LAN-based port scanning, no rogue device on public Wi-Fi probing for it, no risk from a misconfigured firewall rule elsewhere on the network. An internal API that is reachable from anywhere on the LAN depends entirely on that one secret header holding up, forever, against every device that can reach the port. Binding to loopback is the difference between “this is fine as long as nothing else goes wrong” and “this class of problem cannot happen.”

It is also a good reminder if you are building your own local API bridges in PHP tooling, which is more common than it sounds. Tinkerwell, Herd, and a handful of other developer tools in the PHP ecosystem run their own local HTTP or WebSocket servers for editor and IDE integration. The same question applies to all of them: does your server explicitly bind to 127.0.0.1, or are you trusting the default behavior of whatever runtime you built it on?

Upgrading

If you have a NativePHP Desktop app on the 2.x line, get onto 2.2.1 or later:

composer require nativephp/desktop:^2.2

Check what you are actually running before and after:

composer show nativephp/desktop

And if you want to verify the fix yourself rather than take the changelog’s word for it, run your app in development and check what the API port is actually bound to:

# macOS / Linux
lsof -iTCP -sTCP:LISTEN -n -P | grep node

# or, more directly
netstat -an | grep LISTEN

Look for the port NativePHP assigns to the Electron API server (check your app’s debug output or NATIVEPHP_API_URL at runtime). On 2.2.1 and later, it should show 127.0.0.1:<port>, never *.port or 0.0.0.0:<port>.

One more item worth knowing about in the same release cycle: 2.2.1 also loosened the package’s dependency constraints to allow Symfony 8 components, which matters if you are running Laravel 13 alongside a Symfony 8-based package elsewhere in your dependency tree. Composer would have been blocking that combination before this fix.

The takeaway

NativePHP Desktop keeps proving out the pitch: real Laravel, real native windows, without reinventing your stack. But “runs a local server for internal use” is a pattern with sharp edges, and this release is a useful case study in exactly what those edges look like. If you shipped a NativePHP Desktop app before late May 2026, this is not a skippable point release. Bump the dependency, verify the bind address yourself, and move on.

Sources