PHP runs in the application process
Requests travel from the webview to PHP in memory, through a SAPI written in Rust. Nothing listens on 127.0.0.1, so nothing else on the machine can talk to your app.
Nativella runs Symfony, Laravel or plain PHP as a desktop application and, experimentally, on Android and iOS. Built on PHP, Rust and Tauri, it hands every request of the window to a PHP embedded in the same process, so no web server runs and no port opens.
nativella run path/to/your/app
Groceries
Eggs, flour, basil. Ask PHP to remember the rest.
[nativella] Data directory: ~/.local/share/com.acme.notes [php] POST php://localhost/_nativella/event 204 3.1 ms [php] GET php://localhost/ 200 4.8 ms [static] GET php://localhost/app.css 200 [php] POST php://localhost/notes/42 303 5.2 ms [redirect] 303 to /notes/42, replaced by a client-side navigation.
You ship your PHP application and a small Rust shell. The window is the webview the system already has.
Why it is different
Requests travel from the webview to PHP in memory, through a SAPI written in Rust. Nothing listens on 127.0.0.1, so nothing else on the machine can talk to your app.
WebKitGTK on Linux, WKWebView on macOS and WebView2 on Windows. Downloads carry neither a browser engine nor a JavaScript runtime.
PHP behaves as under PHP-FPM for $_SERVER, $_POST, cookies, sessions and header(). An application that runs behind nginx should run here.
nativella build packs the project and PHP into a single executable, so the machine does not need PHP. The same bundle goes into an Android APK or an iOS app.
Plain PHP objects
Every native API is a small, typed PHP class with named arguments, enums and value objects, and it throws an exception when the system refuses. Inject them in Symfony or Laravel, or get them all from Nativella::create().
Native events, such as a menu click, a global shortcut or files dropped on a window, reach PHP as typed objects and the page’s JavaScript as nativella:* DOM events.
use Nativella\Dialog\FileFilter;
use Nativella\Nativella;
$native = Nativella::create();
$path = $native->dialog->openFile(
title: 'Import a note',
filters: [new FileFilter('Markdown', ['md'])],
);
if (null !== $path) {
$id = $notes->import($path);
$native->notification->send('Imported', basename($path));
$native->window->navigate('/notes/'.$id);
}How it works
A Tauri window registers a php:// scheme, and every request of that scheme goes to an embedded PHP instead of the network.
php://localhost/ request to Rust. A file of public/ comes straight back from disk.nativella_call(), wrapped by the PHP package./_nativella/event, which no page can forge.nativella:* DOM events.Native APIs
Open, move, resize and hide windows, each known by its label. Print one to PDF.
$window->open('settings')
The application menu and context menus. Clicks come back as events.
$menu->popup(...$items)
An icon in the system tray, with a menu. The app lives on without windows.
$tray->set('Notes')
Messages, questions, file pickers. The answer is a return value.
$dialog->confirm('Sure?')
Desktop notifications, from any request. A click comes back as an event.
$notification->send('Hi')
Read and write text and HTML, from PHP.
$clipboard->readText()
Keyboard shortcuts that fire while the app is in the background.
GlobalShortcutPressed
A key-value store for preferences, kept as JSON in the data directory.
$settings->get('theme')
Tokens and passwords in the keychain of the system, encrypted by it.
$secrets->get('token')
Wake PHP up for periodic work: a sync, a cleanup, a reminder.
$timer->every('sync', 300)
Start a worker or a tool. Its output and its end come back as events.
ChildProcessOutput $event
Battery level, power source, an event when the computer wakes up, and keeping it awake.
$power->isOnBattery()
Open URLs in the browser, files with their application, folders in the file manager.
$shell->openUrl($url)
Displays, work areas, scale factors and the cursor.
$screen->primary()
The operating system, its locale, and the light or dark theme.
$system->theme()
Push events from PHP to the JavaScript of any window.
$page->dispatch('saved')
Identity, directories, autostart, badge count, quit and restart.
$app->enableAutostart()
Menu clicks, shortcuts, dropped files, focus and theme changes, as typed objects.
MenuItemClicked $event
SQLite in the data directory, migrated when the app starts.
NATIVELLA_DATA_DIR
notes://42 in a browser or a mail opens the app, and PHP gets the URL.
AppUrlsOpened $event
One executable that carries your whole project and its PHP, a Debian package of it, and updates you sign.
nativella build notes
The same project in an APK or an iOS app, experimentally.
castor mobile:android symfony
Vibrations, the location of the phone, fingerprint and face authentication, barcodes, NFC tags, the share sheet.
$biometrics->authenticate('Unlock')
Bring your framework
A File › Export… item is set when the app boots. Its click arrives as a MenuItemClicked event, handled by a Symfony listener, a Laravel listener, or three lines at the top of index.php.
The Symfony bundle and the Laravel package also register the APIs in the container, point the database to the data directory and migrate it at boot.
namespace App\EventListener;
use App\Note\NoteExporter;
use Nativella\Dialog;
use Nativella\Event\MenuItemClicked;
use Nativella\Notification;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener]
final readonly class ExportListener
{
public function __construct(
private Dialog $dialog,
private NoteExporter $exporter,
private Notification $notification,
) {
}
public function __invoke(MenuItemClicked $event): void
{
if ('export' !== $event->id) {
return;
}
$path = $this->dialog->saveFile(fileName: 'notes.zip');
if (null !== $path) {
$this->exporter->export($path);
$this->notification->send('Export finished', $path);
}
}
}namespace App\Listeners;
use App\Notes\NoteExporter;
use Nativella\Event\MenuItemClicked;
use Nativella\Laravel\Facades\Dialog;
use Nativella\Laravel\Facades\Notification;
final readonly class ExportNotes
{
public function __construct(
private NoteExporter $exporter,
) {
}
public function handle(MenuItemClicked $event): void
{
if ('export' !== $event->id) {
return;
}
$path = Dialog::saveFile(fileName: 'notes.zip');
if (null !== $path) {
$this->exporter->export($path);
Notification::send('Export finished', $path);
}
}
}use App\NoteExporter;
use Nativella\Event\EventFactory;
use Nativella\Event\MenuItemClicked;
use Nativella\Nativella;
require dirname(__DIR__).'/vendor/autoload.php';
$native = Nativella::create();
// Null unless the runtime itself sent the request.
if (null !== $event = EventFactory::fromGlobals()) {
if ($event instanceof MenuItemClicked && 'export' === $event->id) {
$path = $native->dialog->saveFile(fileName: 'notes.zip');
if (null !== $path) {
(new NoteExporter($pdo))->export($path);
$native->notification->send('Export finished', $path);
}
}
http_response_code(204);
return;
}
// …the pages of the application, as usual.Getting started
You only do this once. The executable has PHP inside. On Linux, it only needs glibc 2.35 and the system webview, WebKitGTK (its package on Ubuntu, Debian, Fedora, Arch…). For macOS, take nativella-macos-aarch64, or build it from source.
sudo apt install libwebkit2gtk-4.1-0 # Fedora: dnf install webkit2gtk4.1, Arch: pacman -S webkit2gtk-4.1
curl -fLo nativella https://github.com/lyrixx/nativella/releases/latest/download/nativella-linux-x86_64
chmod +x nativella && sudo mv nativella /usr/local/bin/Install the Symfony bundle, the Laravel package or the PHP API on its own from Packagist.
# Symfony
composer require nativella/symfony-bundle
# Laravel
composer require nativella/laravel
# Plain PHP
composer require nativella/nativellaA window opens on your home page. When the app is ready, pack it into one executable.
nativella run my-app
nativella build my-app
./dist/my-appDemos
They live in the repository, and run with nativella run demos/<name> after a composer install.
Symfony · Doctrine · Twig
Markdown notes with an application menu, a tray icon, context menus, a quick-note global shortcut, and files dropped on the window turned into notes.
demos/symfony
Laravel · Eloquent · Blade
A task list that exports through a save dialog, keeps a count in the tray and comes back to the front with a global shortcut.
demos/laravel
Plain PHP · DBAL · no framework
Text snippets copied to the clipboard in one click, deleted after a native confirmation, imported from files picked or dropped.
demos/vanilla
Android and iOS · experimental
These screenshots show the Symfony demo, unchanged, built into an APK with castor mobile:android symfony and running on an Android 16 emulator. It has Doctrine migrations at boot, forms and sessions, Markdown, the phone’s clipboard and a native dialog opened by PHP. A note takes the phone’s location in one method call, and the other demos vibrate and ask for a fingerprint. The CI opens it on every change, in an Android emulator and in the iOS simulator.
iOS runs in the simulator only for now. The PHP inside is the desktop’s, intl included, built for the phone by Nativella.
Status
Nativella started as a proof of concept of PHP, Rust and Tauri in one process, to find out if a Tauri window could talk to PHP without an HTTP server. It could, and the project was built on top of it. It is not finished.
getUserMedia()