Open source · v0.3.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 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

What you don’t ship

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

What is left: your PHP application, a small Rust shell, and the webview the system already has.

Why it is different

A desktop app that is still, underneath, a PHP app.

01

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.

02

The webview of the system

WebKitGTK on Linux, WKWebView on macOS, WebView2 on Windows. No browser engine in every download, no JavaScript runtime next to it.

03

Your front controller, untouched

$_SERVER, $_POST, cookies, sessions, header(): PHP behaves as under PHP-FPM. If it runs behind nginx, it should run here.

04

One file to hand over

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

Native features, one method call away.

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.

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

One process. Two threads. Zero sockets.

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 page asks for a URL. The webview hands the php://localhost/ request to Rust. A file of public/ comes straight back from disk.
  2. PHP runs your front controller. The rest is queued to the PHP thread: a full request through a custom SAPI, with 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. …and reach the page as nativella:* DOM events.

The whole story, and what WebKit does not do

Native APIs

Everything a desktop app reaches for.

Bring your framework

One menu item, three ways.

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.

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

    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.

    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

    From Packagist: the Symfony bundle, the Laravel package, or the PHP API on its own.

    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 apps, one per way of writing PHP.

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

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.

Mobile, in the docs

The Notes demo on an Android phone: a note titled “Hello from Android”, its Markdown rendered as “PHP 8.5 runs in-process.”
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 by PHP
Tapping Delete opens a native Android dialog, “Delete the note”, with Cancel and Delete buttons.
Asked by PHP

Status

Early, and honest about it.

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.

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 and redirects
  • Twenty native APIs, nineteen native events
  • SQLite in the data directory, migrated at boot
  • A single executable carrying the project and its own PHP
  • 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, code signing and updates
  • No screen readers on Linux, to dodge a WebKitGTK crash

Limitations and roadmap

Your next desktop app is already written. In PHP.