Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Desktop Laravel Package

nativephp/desktop

NativePHP for Desktop lets Laravel developers build native desktop applications using familiar PHP and Laravel tools. Package apps for Windows, macOS, and Linux with native windows, menus, filesystem access, notifications, and more—no new language required.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and scaffold first, code second.

composer require nativephp/desktop
php artisan native:install      # downloads the Electron shell + PHP binaries, publishes config
php artisan native:serve        # (alias native:dev) opens the app window with hot reload

native:install is the important step to understand: it publishes config/nativephp.php, creates a nativephp/ directory containing the Electron main/preload process and its package.json, and drops a NativeAppServiceProvider into app/Providers. That triple is your entire extension surface — the config for windows, the provider for boot-time native calls (menus, tray, shortcuts), and the JS folder for anything the PHP bridge doesn't expose.

Where to look first, in order:

  1. config/nativephp.php — window defaults, app version, updater provider, bundled PHP binary settings.
  2. app/Providers/NativeAppServiceProvider::boot() — this is where a desktop app "starts". Menus, tray icons, global shortcuts, and the first Window::open() live here.
  3. routes/web.php — the app is still just Laravel served over HTTP; your existing routes render inside the shell.
  4. config/logging.php + storage/logs/laravel.log — your primary debugger (see gotchas).

The canonical first use case is wrapping an existing internal Laravel app (dashboard, admin panel, CLI companion) into a desktop binary: SQLite database, native menu, system-tray icon, and desktop notifications. Start there rather than a greenfield app — it exercises everything (routing, DB, storage, native APIs) without forcing you to learn the whole API surface at once.

Minimum viable loop:

// NativeAppServiceProvider::boot()
Window::open()
    ->id(1)
    ->title(config('app.name'))
    ->width(1280)->height(800)
    ->rememberState();          // window position/size persisted across launches

Then php artisan native:serve, hit the native menu, drop a Notification::title('Hi')->show() in a controller, and you've validated the whole pipeline including the PHP→Electron bridge.

Implementation Patterns

Keep the browser path alive. The highest-leverage habit with this package is to treat native as progressive enhancement. Put every native call behind a small service (App\Native\Ui) that checks whether it's running under the shell, so php artisan serve still works and your feature tests don't explode:

class Ui {
    public function notify(string $title, string $body): void {
        if (! $this->isNative()) return;      // System::isNative() or a config flag
        Notification::title($title)->body($body)->show();
    }
}

You get fast iteration in the browser, CI-able tests, and a single seam to swap in a fake.

Event-driven glue, not polling. The Electron main process dispatches Laravel events into your app; the idiomatic pattern is to build menus/trays with stable IDs in the service provider and handle clicks in listeners:

// boot()
Menu::create()
    ->item('New Invoice', 'new-invoice')
    ->item('Preferences', 'prefs')
    ->separator()
    ->item('Quit', 'quit');

// app/Listeners/HandleMenuItemClicked.php
match ($event->id) {
    'new-invoice' => Window::open()->url('/invoices/create'),
    'prefs'       => Window::open()->route('settings'),
    'quit'        => App::quit(),
};

The same pattern applies to WindowClosed, WindowFocus, TrayClicked, NotificationClicked, GlobalShortcut — all are ordinary Laravel events, so they queue, test, and dispatch like anything else.

Multi-window as a real feature. Each Window::open() is a separate BrowserWindow pointed at the same Laravel server: sessions and DB are shared, JS state is not. Use this for the "main window + detached inspector/preferences" layout rather than modals, and use ->route('name') or ->url() with relative paths so it works in dev (Vite on :5173) and prod (bundled server on an ephemeral port) alike.

Tray-first for utilities. For menu-bar/status apps, combine Tray::create()->icon(...)->tooltip(...) with a config window that's hidden instead of closed — intercept WindowClosed and call Window::hide() unless a real quit was requested. This is the shape most internal tools want, and it's where the package feels genuinely native rather than "a browser in a box."

Native capability cheat-sheet worth wiring early because they're each one-liners: Clipboard::text()/->write(), Dialog::openFile()/->saveFile(), Shell::openExternal($url) (never window.open), Settings for persistent key/value prefs, Screen for multi-monitor geometry, Power/System for idle and OS info, Process to spawn CLI helpers.

Queue work where the events can be seen. The PHP process spawned by the shell is the one that can emit UI events. If you run queue:work in a separate process, its Notification::show() calls go nowhere. Either run the worker from NativeAppServiceProvider via Process, or use database queue + a worker spawned as part of the app lifecycle, and accept that desktop apps are single-user: prefer synchronous work plus progress events over heavyweight queue infrastructure.

Data lives in user-space, not the bundle. Point SQLite at a writable per-user directory and run migrate --force on first boot (or ship a seeded DB via a publish step). Everything under base_path() should be treated as read-only in the packaged app — that includes .env, storage/, and any file you'd normally let Laravel write.

Ship updates as a feature, not an afterthought. The updater is built in: bump the version, native:publish to produce release artifacts, and call Updater::checkForUpdates() at boot or from a menu item. Wire it on day one against a staging repo — retrofitting it after users are on unsigned, unversioned builds is painful.

CI matrix is the build system. Builds are per-platform; encode native:build for macos-latest, windows-latest, and ubuntu-latest in GitHub Actions with secrets for signing, and let CI be the only place you produce distributables. Locals builds are for smoke-testing only.

Gotchas and Tips

  • You are debugging two applications. PHP errors go to storage/logs/laravel.log; Electron/JS errors go to DevTools (open with a keyboard shortcut or --debug). dd()/dump() output is swallowed by the shell — use Log::info() and watch the log file.
  • .env is baked in at build time. Any secret in your environment ships inside the binary. Keep API keys server-side, or gate privileged calls behind your Laravel app's auth; never bundle a secret you wouldn't paste into a public repo.
  • Two environments that behave differently. native:serve is hot-reloading against your local dev server and .env; native:build is a frozen snapshot with its own config. Bugs that only appear in builds are usually route caching, .env drift, or absolute URLs — always smoke-test an actual native:build before shipping.
  • Code signing will consume more time than the app did. Unsigned macOS builds get Gatekeeper-blocked and Windows builds trip SmartScreen and antivirus heuristics. Notarization and a Windows certificate are prerequisites, not polish.
  • Service provider changes need a restart; views hot-reload. native:serve will happily reload a Blade view while silently ignoring your new menu because NativeAppServiceProvider::boot() ran once at launch. Restart the shell before concluding an API doesn't exist.
  • The macOS Edit menu is not optional. If your app menu lacks the standard Cut/Copy/Paste/Select-All roles, keyboard shortcuts won't work in text fields — a classic Electron trap that looks like a Laravel bug.
  • Don't assume base_path() is writable. The .app bundle and Program Files install dirs are read-only. Redirect database, storage, and any uploaded/exported files to a user-data path; otherwise you get permission errors that only reproduce on other people's machines.
  • Keep custom JS minimal and documented. Anything the PHP facade doesn't expose means editing nativephp/electron/*.js (main/preload). Those files are regenerated on upgrade — the more you patch, the more painful composer update becomes. Prefer feature requests, a thin ipcRenderer.invoke in preload plus a small PHP wrapper, and a note in the README.
  • Memory/GC for tray and shortcuts. Hold references to tray and window objects; a tray icon created and dropped inside a method can disappear. Menu item IDs must be unique and stable across windows.
  • Cross-platform is not free. Global shortcuts, tray behavior, window chrome, and file dialogs differ per OS; test on all three (CI + a VM) before claiming support. Linux packaging (AppImage/deb) has the widest variance.
  • Expect breaking changes. The package is young — the facade namespace and provider layout have already shifted between major versions. Pin the version in composer.json, keep native code isolated in app/Native + one provider, and read the upgrade guide before bumping. The escape hatch is that everything is still Laravel: if a native API regresses, you can often fall back to Shell::openExternal() or a web route while you wait for the fix.
  • Don't fight the size. A bundle with PHP + Electron runs ~150–250 MB. For internal tools that's irrelevant; for consumer distribution, budget for it in your download story.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core