In-process, not over HTTP
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 on Android and iOS, experimentally. The window hands every request straight to PHP, inside the same process: no web server, no open port, no Electron, no Node.
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.
What is left: your PHP application, a small Rust shell, and 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, WebView2 on Windows. No browser engine in every download, no JavaScript runtime next to it.
$_SERVER, $_POST, cookies, sessions, header(): PHP behaves as under PHP-FPM. If it runs behind nginx, it should run here.
nativella build packs the project and PHP into a single executable: the machine needs no 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: named arguments, enums, value objects, and exceptions when the system says no. Inject them in Symfony or Laravel, or get them all from Nativella::create().
The other way around, what happens natively reaches PHP as typed events: a menu click, a global shortcut, files dropped on a window. 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, and an event when the computer wakes up.
$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.
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.
$biometrics->authenticate('Unlock')
Bring your framework
A File › Export… item, set when the app boots. Its click arrives as a MenuItemClicked event: 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
Once. One executable, with PHP inside: on Linux, it only needs glibc 2.35 and the webview of the system, 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/From Packagist: the Symfony bundle, the Laravel package, or the PHP API on its own.
# 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
The Symfony demo, unchanged, built into an APK with castor mobile:android symfony and running on an Android 16 emulator: Doctrine migrations at boot, forms and sessions, Markdown, the clipboard of the phone, and a native dialog opened by PHP. A note takes the location of the phone in one method call; 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.
Early days: iOS in the simulator only. The PHP inside is the desktop’s, intl included, built for the phone by Nativella.
Status
Nativella started as a spike: can a Tauri window talk to PHP without an HTTP server? It can. What it is not yet is finished.