Open source · v0.5.0 · desktop, Android and iOS (experimental)

Your PHP app, in a window of its own.

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

What you don’t ship

  • nginx
  • PHP-FPM
  • localhost:8000
  • a copy of Chromium
  • Node.js
  • Electron

You ship your PHP application and a small Rust shell. The window is the webview the system already has.

Why it is different

A desktop app that remains a PHP app.

01

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.

02

The system webview

WebKitGTK on Linux, WKWebView on macOS and WebView2 on Windows. Downloads carry neither a browser engine nor a JavaScript runtime.

03

Your front controller, untouched

PHP behaves as under PHP-FPM for $_SERVER, $_POST, cookies, sessions and header(). An application that runs behind nginx should run here.

04

A single executable

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

Native features are PHP method calls.

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.

Lifecycle and events

src/Import.phpPHP
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

PHP and the window share one process.

A Tauri window registers a php:// scheme, and every request of that scheme goes to an embedded PHP instead of the network.

How a request travels in Nativella One process. The webview hands a php://localhost request to the Rust scheme handler, which serves public files from disk and queues the rest to the PHP thread. The runtime keeps cookies and follows redirects, the custom SAPI runs public/index.php. PHP calls native APIs with nativella_call(). Native events go back to PHP as internal requests to /_nativella/event, and to the page as nativella:* DOM events. nativella · one process no HTTP server · no TCP port WEBVIEW RUST · MAIN THREAD PHP THREAD Notes php://localhost/notes WebKitGTK · WKWebView WebView2 page JavaScript nativella:menu.itemClicked php:// scheme handler Tauri 2, asynchronous static files public/*, read from disk native APIs windows · menus · dialogs · tray… native events clicks · focus · shortcuts · drops runtime queue · cookie jar · redirects custom SAPI ext-php-rs · libphp (embed) public/index.php your application, untouched: Symfony, Laravel, plain PHP request response 1 a file? queued 2 nativella_call() 3 POST /_nativella/event 4 DOM event 5
  1. The webview hands the php://localhost/ request to Rust. A file of public/ comes straight back from disk.
  2. The other requests are queued to the PHP thread, which runs your front controller through a custom SAPI. The runtime adds the cookie jar and the redirects WebKit skips for custom schemes.
  3. PHP calls native code with nativella_call(), wrapped by the PHP package.
  4. Native events come back as internal requests to /_nativella/event, which no page can forge.
  5. They also reach the page as nativella:* DOM events.

How it works in detail, and what WebKit does not do

Native APIs

The APIs a desktop app needs.

Bring your framework

A menu item in Symfony, Laravel and plain PHP.

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.

Symfony Laravel Plain PHP

src/EventListener/ExportListener.phpPHP
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);
        }
    }
}

Getting started

From a PHP project to a window, in three steps.

  1. Download the runtime

    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.

    Shell
    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/
  2. Add the package

    Install the Symfony bundle, the Laravel package or the PHP API on its own from Packagist.

    Shell
    # Symfony
    composer require nativella/symfony-bundle
    # Laravel
    composer require nativella/laravel
    # Plain PHP
    composer require nativella/nativella
  3. Run it

    A window opens on your home page. When the app is ready, pack it into one executable.

    Shell
    nativella run my-app
    nativella build my-app
    ./dist/my-app

Installation, step by step

Demos

Three small demo applications.

They live in the repository, and run with nativella run demos/<name> after a composer install.

  • Symfony · Doctrine · Twig

    Notes

    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

    Tasks

    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

    Snippets

    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 Notes demo on a phone.

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.

Mobile documentation

The Notes demo on an Android phone: a note titled “Hello from Android”, its Markdown rendered as “PHP 8.5 runs in-process.”
Markdown rendered by PHP
After tapping Copy, the page says “Copied as Markdown.”, and the clipboard preview of Android shows the Markdown source that PHP copied.
Copied to the clipboard by PHP
Tapping Delete opens a native Android dialog, “Delete the note”, with Cancel and Delete buttons.
A native dialog opened by PHP

Status

What works and what does not.

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.

0ports opened
~5 msa Symfony page with Twig and a session, kernel booted every time
1executable for the whole project

Works today

  • Symfony, Laravel and plain PHP, front controller untouched
  • Forms, sessions, cookies, redirects and downloads
  • Twenty-four native APIs, twenty-two native events
  • SQLite in the data directory, migrated at boot
  • A single executable carrying the project and its own PHP, which updates itself
  • Debian packages, on Linux
  • Android, experimentally: the three demos, in arm64 APKs
  • iOS, experimentally: the Symfony demo, in the simulator

Not yet

  • macOS: no window opened yet; Windows: never built; iOS: no device
  • One PHP thread: a dialog holds back every other request
  • No worker mode: the framework boots on every request
  • Installers for macOS and Windows, code signing
  • No PHP API for photos or the microphone: pages use getUserMedia()
  • No screen readers on Linux, to dodge a WebKitGTK crash

Limitations and roadmap

Try it on an application you already have.