pad pack puts a whole application - its pages, its PHP, its www/ files - with the PAD engine and PHP compiled to WebAssembly into one directory. Opened from the disk it runs in the browser tab; put on any https host it installs as an application and works offline. There is no PHP server at all, and what the application writes - its SQLite database first - is kept in the browser between visits.

pad pack --install            # once: what it packs with, in ~/.cache/pad-wasm
pad pack notes                # DATA/wasm/notes/
pad pack notes --serve        # served on http://127.0.0.1:8090/ - installable
pad pack notes,ledger,kanban  # several, with a launcher
PHP
HTML
fragments/offline_applications_1.pad
{diagram direction='TB', title='How a packed application answers'}
P([A page, its fetch, its EventSource]) --> W[The service worker - or, from the disk, the page around the frame]
W --> R[The runtime: pad-app.js]
R -->|a www file| B[(The bundle)]
R -->|a page| H[PHP as WebAssembly, the PAD engine]
H --> S[(SQLite under DATA)]
S -.->|after each request| I[(IndexedDB)]
{/diagram}
Result
How a packed application answersA page, its fetch, its EventSource → The service worker - or, from the disk, the page around the frame; The service worker - or, from the disk, the page around the frame → The runtime: pad-app.js; The runtime: pad-app.js → The bundle (a www file); The runtime: pad-app.js → PHP as WebAssembly, the PAD engine (a page); PHP as WebAssembly, the PAD engine → SQLite under DATA; SQLite under DATA → IndexedDB (after each request)A page, its fetch, its EventSource → The service worker - or, from the disk, the page around the frameThe service worker - or, from the disk, the page around the frame → The runtime: pad-app.jsThe runtime: pad-app.js → The bundle (a www file)The runtime: pad-app.js → PHP as WebAssembly, the PAD engine (a page)PHP as WebAssembly, the PAD engine → SQLite under DATASQLite under DATA → IndexedDB (after each request)a www filea pageafter each requestA page, its fetch, itsEventSourceThe service worker - or,from the disk, the pagearound the frameThe runtime: pad-app.jsThe bundlePHP as WebAssembly, thePAD engineSQLite under DATAIndexedDB

Two ways to run it

Opened from the diskServed over https, or on localhost
Startdouble-click index.htmlopen the address; the browser offers to install it
PHP runsin the tabin the service worker, for every window
The pagesin a frame filling the window, the address after the #at their own addresses
Offlinealwaysafter the first visit

Both answer every request as a server would: cookies are kept - sessions, CSRF tokens, flash messages, a login - any method and header of the page's own JavaScript is passed on, an upload arrives in $_FILES, an answer may be binary or a download.

What is kept

Everything under DATA/ - SQLite databases, uploads, JSON files, sessions - is written to the browser's IndexedDB after every request that changed it, and put back when the application starts again. A new pack of the same applications finds that data: an application that keeps its schema in _migrations/ and runs padMigrate() when one is pending brings the database a visitor kept up to date - the ledger application does.

The database

PHP in the browser has SQLite and no MySQL. An application that is SQLite already (padSqlDriver=sqlite, see Sqlite) runs as it is. One written for MySQL on its server can switch for the browser in _offline.json, whose env the runtime passes where padEnv looks first:

{
  "name": "Shop", "short": "Shop", "theme": "#0f766e",
  "icon": "icon.svg",
  "icons": { "192": "icon-192.png", "512": "icon-512.png" },
  "env": {
    "padSqlDriver": "sqlite",
    "padSqlDatabase": "shop/shop.sqlite",
    "padSqlSetup": "_install/schema.sql"
  }
}

.env files are never packed: they hold a server's passwords, and whoever has the pack can read it.

Import and export

The badge in a corner of every page - or Ctrl+Shift+K - opens the runtime's panel. Its Database tab browses the tables, runs SQL, exports a database as the .sqlite file or as SQL that builds it again, and imports either - SQLite checks what comes in before it replaces anything. Its Storage tab backs up all of DATA/ as one zip and puts such a zip back. An application can offer the same from its own pages, on a server as in the browser: the database file with padUploadSend, its rows as JSON or CSV with $padExpose - the notes application's backup page does both.

The page's own requests

fetch, XMLHttpRequest, a {live} region, an {ajax}, htmx asking for a {fragment}, an EventSource - they all reach PHP in the browser. One PHP answers every request of the application, so a stream that held it would hold the application: padSse answers one round per connection there, and EventSource comes back after the interval by itself, with the last event's id - see Server sent events. A page that should behave differently in the browser asks padWasm():

PHP
fragments/offline_applications_2.php
<?php

// padWasm() is TRUE on PHP compiled to WebAssembly - in
// a browser, an application packed by pad pack - and
// FALSE on a server.

$where = padWasm () ? 'in this browser' : 'on the server';

$mail = padWasm () ? 'kept until the application is online'
: 'sent at once';

?>
HTML
fragments/offline_applications_2.pad
<p>This page runs {$where}: a message written here is {$mail}.</p>
Result

This page runs on the server: a message written here is sent at once.

Limits

The applications

Three applications are made to show it, each running on this server as well:

pad pack notes,ledger,kanban packs them together, with a launcher. How it is built - the runtime, the service worker, the panel - is in wasm/README.md.