apps/softn-single-private hosts one .softn application from a PHP web
server. The archive stays in a private directory the web server never
serves. Visitors receive a page rendered by PHP, then the runtime fetches
what the application needs to run — its UI and logic text in one request,
each image, sound, model or font as its own request when the application
renders it. The application's scripts run in the same ZIPP WebAssembly
sandbox and the same renderer as every other SoftN host. No .softn file,
and no request that returns one, exists on the site.
It is the same shell as the static single-app runtime: a spinner, a non-blocking permission bar, the application's own UI unbranded. What differs is delivery.
| Request | Answer |
|---|---|
index.php |
The page, rendered on the server: title, language, theme colours, description, favicon link, loading text and a small boot configuration. Sets the viewer cookie. |
index.php?source |
The source pack: the deployment's settings, the manifest reduced to the fields the runtime reads, every text entry (.ui, .logic, .json, .xdb, …) and the names of the rest. |
index.php?entry=PATH |
One binary entry with its MIME type, an ETag, a private cache policy and byte ranges, so <video> and <audio> can seek. |
index.php?icon |
The manifest icon, for the favicon. |
index.php?manifest |
The web app manifest for installation, built from the deployment settings; no cookie needed. |
Every page and asset the shell serves carries Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: credentialless, set by
softn-serve.php itself and repeated in both .htaccess variants, so the
app is as cross-origin isolated here as on softn.com (SharedArrayBuffer
for the CPU language-model provider). Nothing to configure on nginx.
The manifest is never sent raw: config.server, with any token in it, and
every field the runtime does not read are dropped. Entries listed under
withhold in the configuration are neither packed nor served. A bundle's
server/ folder is withheld whatever withhold says: if the full private
server bundle is deployed as app.softn, its server logic and SQL are never
packed or served, because they are the backend's and not the page's. Text-form
models (.gltf, .obj) are served as entries, not packed, so a large scene
does not sit inside the JSON the application boots from.
The pack and the entries require the cookie index.php sets (signed,
HttpOnly, SameSite=Strict, twelve hours by default), refuse browser
navigations and cross-site fetches by their Sec-Fetch-* headers, and answer
only at the page's own address. Opening index.php?entry=… in a tab, hot
linking an entry from another site, or fetching the pack with a script that
never loaded the page all answer 403.
This is delivery control: the archive is not on a URL, nothing the
application does not use leaves the server, and a visitor's browser gets the
pieces one request at a time under a cookie the page issued. It is not copy
protection. A browser must receive the UI and logic text to execute it, and
must receive an image to draw it; a visitor with developer tools can read the
pack and save entries as they arrive. What they cannot do from this site is
download the .softn file, reach entries the application never asked for,
or see the manifest's server configuration. For access control, put the
whole deployment behind server-side authentication as well; for secrets and
proprietary logic, keep them on a server. The
static runtime's distribution limits
apply here in full.
Requirements on the server: PHP 8.1 or newer with the zip extension, and
any web server that runs PHP. Apache with .htaccess is what the included
rules target; other servers need the equivalent (index.php as the directory
index, .wasm as application/wasm, .mjs as JavaScript, and nothing under
private/ reachable).
From the repository, with the CI toolchain (Node 24.19+ with npm 10):
npm ci
npm run build:packages
npm run build -w @softn/single-private
npm run package:single-private
release/softn-app-private-vVERSION.zip holds two folders:
webroot/—index.php,softn-serve.php,.htaccessand the runtime'sassets/. Upload its contents to the public directory the application should live at, root or subdirectory.private/—app.softn,serve.config.php,shell.htmland a deny-all.htaccess. Put it beside the public directory, outside every document root. If it cannot be a sibling, set$privateat the top ofindex.phpto its absolute path. The PHP user needs write access to it once, to generatesecret.keyanddigest.cache; otherwise setsecretin the configuration and copy the pin fromsha256sum app.softn.
Replace private/app.softn with your application and edit
private/serve.config.php:
<?php
return [
'id' => 'my-application',
'title' => 'My application',
'bundle' => __DIR__ . '/app.softn',
'theme' => 'dark',
'loadingText' => 'Loading…',
'permissionMode' => 'prompt',
'viewerToken' => true,
'tokenLifetime' => 43200,
'secret' => '',
'withhold' => [],
'cacheSeconds' => 3600,
];idnames the visitor's local XDB records together with the page's path. Keep it stable across updates of the same application.permissionModepromptshows the permission bar;preapprovedenables the declared capabilities at once and requiressha256, the hex digest of the archive, which the host also checks before serving the pack.permissionsmay name a JSON permission declaration that replaces the bundle'spermission.json; the precedence is the static runtime's.viewerTokenmay be turned off for a deployment that is already behind its own authentication, or one that serves the application into another site's page.secretmay be set explicitly when several servers share an application or the private directory is read-only.withholdlists entries never sent: exact paths, directories with a trailing slash, or globs. The manifest cannot be withheld.descriptionandlangfill the page's<meta name="description">and<html lang>; without a description the manifest's is used.allowPrivateInWebrootlifts the refusal to run when the private directory is inside the document root, for a host that can only offer one folder; its.htaccessthen has to be honoured.frameAncestorslists the other sites allowed to show the page in a frame (origins such ashttps://portal.example.com). By default only the page's own site may frame it (frame-ancestors 'self',X-Frame-Options: SAMEORIGIN): a page any site can frame can have its permission bar covered with a look-alike and its Allow clicked for the visitor.
The sample bundle is generated only when dist/private/app.softn is absent,
and the sample configuration only when dist/private/serve.config.php is
absent; neither overwrites an operator's file. Runtime bounds: an entry up to
50 MB, a source pack up to 32 MB decoded, an icon up to 256 KiB, sixty
seconds to fetch the pack.
The page is a Progressive Web App out of the box. index.php renders a manifest link, the Apple and Android install tags, Open Graph and Twitter card tags, and registers sw.js; index.php?manifest answers with a web app manifest built from the deployment's title, description, lang and theme. The archive holds placeholders carrying the SoftN mark: webroot/pwa-icons/icon-192.png, icon-512.png and icon-maskable-512.png (the installed-app icons), webroot/apple-touch-icon.png (iOS home screen) and webroot/share.png (the 1200 × 630 picture shown when the link is posted). Replace them with your own artwork under the same names, or point pwa.icons, pwa.appleTouchIcon and pwa.shareImage at other paths.
serve.config.php accepts 'pwa' => true (the default), false, or an array: shortName (up to 30 characters under the icon), themeColor and backgroundColor (#rrggbb), display, orientation, icons (a list of src, sizes, type, optional purpose), appleTouchIcon, shareImage, shareImageWidth, shareImageHeight, siteUrl (the public address of the page directory, for previews behind a proxy or CDN that hides the host name) and serviceWorker. Paths are relative to the page's directory, so a deployment under /app/ works unchanged; absolute http(s) URLs pass through.
The service worker caches only the runtime's hashed assets/, the icons and the share image, and answers navigations network-first with the last page as an offline fallback. It never caches index.php with a query, so the source pack, entries and the manifest always come from the server and a redeployed application is seen on the next visit. Ubuntu's Apache maps /icons/ to its own directory-listing images; the placeholders live in pwa-icons/ so they never collide with it.
npm run package:private-single-php produces
release/softn-app-private-with-backend-linux-x64-vVERSION.zip: the same webroot/
and private/ with api.php and a .htaccess that also routes /api/...,
plus the backend/ folder of the static variant's backend archive (bundled
Linux x64 Node, SQLite and ZIPP WASM). The application is served privately
with or without the backend; enabling it follows the included DEPLOYMENT.md,
which is apps/softn-single-private/PRIVATE_DEPLOYMENT.md
(README.md beside it is the short explainer every release archive carries).
CI verifies that archive with the backend checks and an Apache smoke test
that also loads the served page, the pack and the refusals.
npm run preview -w @softn/single-private serves dist/webroot with PHP's
built-in server on port 1456 (PORT and PHP override). npm run dev
builds first. Under the built-in server .htaccess is not read, so it stands
in for Apache's PHP handling only; the host itself answers 404 for any path
that is not its own, which is why /app.softn is not found there either.
npm test -w @softn/single-private covers the boot configuration, the
asset resolver's same-origin URLs and pathOf, the pack parser and loader,
the shell template, and the equality of the PHP extension table with
@softn/core's registry. With php on PATH it also starts the built-in
server over a temporary deployment and checks the rendered shell, the cookie,
the pack's contents and what it omits, entry headers, conditional and range
requests, a nine-megabyte streamed entry, the icon, every refusal, the
digest pin, the sidecar and the generated secret. Without php that suite
is skipped.