nativephp/electron
NativePHP for Electron lets Laravel developers build and ship native desktop applications from a single PHP codebase. It bundles a PHP runtime with Electron to create cross-platform Windows, macOS, and Linux apps using familiar Laravel tools—no separate desktop stack required.
NativePHP/Electron is the desktop shell that lets an existing Laravel app ship as a native Electron application. The day-to-day workflow is: keep writing Laravel, and reach for the Native\Laravel facades only where the desktop layer needs to exist (windows, tray, menus, notifications, auto-updater).
Install and boot
composer require nativephp/electron
php artisan native:install # publishes config + scaffolds electron app
php artisan native:serve # or native:dev for watching
Where to look first
config/nativephp.php — the source of truth for windows, updater, and app metadata.app/Providers/NativeAppServiceProvider.php — the lifecycle hook (boot) where you register windows, menus, and tray icons.Native\Laravel\Facades\Window — the entry point for almost every first use case.First use case: open a single window pointed at a named route, so the desktop app is just your existing UI:
// NativeAppServiceProvider::boot()
Window::open()
->route('dashboard')
->width(1280)
->height(800);
If that renders, you already have a shippable app. Everything below is incremental polish on top of that baseline.
Windows as first-class citizens. Treat window definitions as configuration, not code scattered through controllers. Define the main window in the service provider, and open secondary windows explicitly by ID so you can focus/close them later rather than spawning duplicates.
Suppressing sub-windows (1.3.0). target="_blank" links and window.open() calls previously always produced a second Electron window — usually a blank one with no Laravel session. The new option lets you suppress that opening and handle the intent yourself (redirect the main window, open a modal, or hand off to the OS browser via shell.openExternal). This is the correct pattern for external links and for OAuth/SSO popups you'd rather run in the system browser.
Navigation guards (1.3.0). You can now prevent the renderer from navigating away from the current domain or page. Use it as a cheap security boundary: pin the app to your own origin so an injected link or a stray window.location assignment can't turn your desktop app into a browser pointed at arbitrary content. Mirror the same intent in the system browser for genuinely external destinations.
Zoom factor on Windows (1.3.0). High-DPI Windows machines and accessibility users benefit from setting an explicit zoom factor per window instead of relying on the default. Note this is a Windows-side capability — don't build UI that assumes it works identically on macOS, and prefer setting it at window creation over mutating it later.
Locale information (1.3.0). New Electron API endpoints expose the OS/app locale. Wire this into App::setLocale() early in the boot path (or on a settings change) so dates, number formatting, and translations match what the user's machine reports, rather than hardcoding a locale in config. Keep Laravel's app.locale as the fallback when the API returns something you don't ship translations for.
Auto-updater discipline. The 1.3.0 change to only read publisher config when the updater is enabled means you can leave publisher keys in config without them affecting builds that don't use the updater. Still, treat updater config as environment-specific: separate builds for internal/beta/stable channels, and never ship a build with the updater pointed at a repo you don't control.
Scheduler and long-running work. Stray scheduler processes being cleaned up is a reminder that the Electron app hosts child PHP processes. For anything recurring, prefer Laravel's scheduler invoked from within the app lifecycle over ad-hoc Process calls, and make your job handlers idempotent — a process killed mid-run should not corrupt state on restart.
Multiple apps on one machine. Running two NativePHP apps side by side no longer fights over the same port on Windows. If you maintain several internal tools, this is what makes "install all of them" viable — but you still want each app to have a distinct app_id, updater feed, and window title so users can tell them apart in the taskbar.
Integration tips. Push anything sensitive (tokens, licensing, sync) through normal Laravel HTTP controllers rather than doing it in the renderer; the renderer is your Blade/Inertia/Livewire UI, and the backend is just Laravel. Use native:build in CI on a matrix per OS, and version the desktop app in lockstep with the web app's API contract.
package.json and re-test native:build for every target OS before releasing. This is the most likely source of a "worked yesterday" build break.native:serve failing to bind is nearly always a leftover process, not a config error.NativeAppServiceProvider boot hook and the Native\Laravel facades are the sanctioned surface. Reaching into the generated Electron JS directly works but will be overwritten by native:install/upgrades — wrap any custom JS in the documented hooks instead.storage/logs/laravel.log and the terminal running native:serve. When something is "blank," check devtools first — it's usually a renderer error or an unreachable dev server, not a Laravel exception.How can I help you explore Laravel packages today?