The envelope
A pwapp is two things: an envelope (the app as data — plain JSON files
under pwpack/) and a runtime that runs it (the runner, the renderer, and
the block bundles the pages use). The runner never compiles the envelope in;
it fetches the files at load time from a folder path or an absolute URL, so
the same package works at a host root, under a sub-path, or on a CDN.
Every claim here is taken from the engine and platform source; each section
names its file. What the code leaves open is under Unverified.
Repo-relative paths below are under pwfabric-core/packages/runner/src/
unless a longer path is given.
Two downloads, one envelope
| Button | File | Contents | Runs by itself? |
|---|---|---|---|
| Export | <slug>.zip | envelope under pwpack/ plus index.html, assets/, shims/, blocks/, engines/ | yes — unzip and serve the folder |
| Download | <slug>.pwpack.tgz | envelope only: project.json, navigation.json, connections.json, appearance.json, surfaces/*.receipt.json, auth.json when the app has a door | no — you supply the runtime and a block manifest |
The tgz has no package/ prefix and no pwfabric.manifest.json; paths are
relative to the archive root. The zip places the same files under pwpack/.
Source: PhiWebs API (closed source): phico-projects.ts
(POST /:id/export, POST /:id/download-pwpack),
PhiCo publish panel (closed source),
PhiWebs API (closed source): phico-project-service.ts
(buildPwappEnvelope).
Layout of an exported pwapp
index.html the single shell; imports the runner, calls runApp()
assets/embed.js the renderer (defines window.PhiWebs.render)
assets/runner.js @phimajor-solutions/pwfabric-runner, ES module
assets/theme.css the app's token set as CSS custom properties
shims/*.mjs import-map targets for react, zod, @phimajor-solutions/*
blocks/manifest.json block type -> bundle file + SHA-256
blocks/<type>.<hash8>.mjs one ESM bundle per block type the pages use
engines/manifest.json connector/provider engines (bundled or declared)
engines/<atomId>.<hash8>.mjs bundled engines only
pwpack/project.json pages, routes, identity (required)
pwpack/navigation.json navbar links (optional)
pwpack/appearance.json appearance config (optional)
pwpack/connections.json data connections (optional)
pwpack/auth.json who the visitor is (optional)
pwpack/surfaces/<key>.receipt.json one receipt per page
pwpack/media/<file> platform media rehosted into the envelopeThere is no per-page HTML. The runner fetches project.json and
blocks/manifest.json (both required), then navigation.json,
appearance.json, auth.json, connections.json and
engines/manifest.json, each of which may be absent.
Source: PhiWebs API (closed source): web-app-assembly-service.ts
(assembleWebApp, buildShellHtml),
pwfabric-core/packages/runner/src/run-app.ts (the Promise.all at the top of runApp).
project.json
The runner’s read contract is PwappProject. Two fields are required.
| Field | Type | Required | Meaning |
|---|---|---|---|
entrySurfaceId | string | yes | page key of the entry page (/ and #/ resolve here) |
surfaceOrder | string[] | yes | every page key, in order; this is the route table |
surfaceReceiptFiles | Record<key, path> | no | page key → receipt path relative to source; absent ⇒ surfaces/<key>.receipt.json |
schemaVersion | string | number | no | the platform writes "1.0.0" |
id | string | no | the platform writes the project slug; keys the app session in session storage |
title | string | no | document title, and the prefix of every page title |
identity | { required, topology?, methods? } | no | required: boolean; topology is 'platform' or 'external'; methods only when topology === 'platform' |
The platform also writes worldId and projectId. The runner does not read
them; they pass through for the app’s own record API. Despite the names,
entrySurfaceId and surfaceOrder hold page keys (short route names such
as home, people), not surface ids.
Source: pwfabric-core/packages/runner/src/types.ts (PwappProject),
pwfabric-core/packages/runner/src/helpers.ts (receiptFilesOf),
phico-project-service.ts (projectJson in buildPwappEnvelope).
Pages, routes and links
A page is a key in surfaceOrder; its receipt is surfaceReceiptFiles[key].
Routing is one rule in both modes: the location either names a key in
surfaceOrder, or it names nothing and the runner renders its not-found
page. It never falls back to the entry page.
- hash mode (default):
#/<key>;#/and no hash mean the entry page.#sectionwithout a slash is an in-page anchor and is left to the browser. - path mode:
<base>/<key>, where<base>is the shell’s<base href>. Needs an SPA fallback on the host and<meta name="pw-router" content="path">.
Links inside receipts are written root-relative to the app: /people,
/people/, or / for the entry page. At render time the runner rewrites
every internal string under a href prop or any prop ending in Href
(ctaHref, linkHref) into the active router’s form. Absolute-scheme URLs
(https:, mailto:) and fragment links (#…) are left alone. An unknown key
is still rewritten, so the click lands on the in-app not-found page.
Source: pwfabric-core/packages/runner/src/router.ts,
pwfabric-core/packages/runner/src/helpers.ts (pageKeyOf, isHrefKey, rewriteHrefs).
surfaces/<key>.receipt.json
A receipt is the render input for one page. The runner reads:
| Field | Type | Notes |
|---|---|---|
blocks | BlockNode[] | required; each node has type, props, optional children |
title | string | optional; composed with project.title unless it already starts with it |
appearance | { config: {...} } | optional; a ref object with the page’s resolved theme |
capabilities | { capability, config? }[] | optional; passed to the renderer as-is (data-bound blocks derive their sources from it) |
The platform additionally writes description, readme, tags and
intent; the runner ignores them. Blocks are referenced only by type;
that string is the join key into blocks/manifest.json.
Source: types.ts (SurfaceReceipt, SurfaceBlockNode), run-app.ts (route()),
phico-project-service.ts (collectFrozenPages).
Atoms: how blocks reach the page
A pwapp ships no atom source. Of the nine atom kinds the contracts define
(block, surface, capability, ui-appearance, pattern, icon-set,
connector, provider, auth-provider), two travel in an exported app, both
as compiled ESM: blocks (one bundle per block type any page uses, listed
in blocks/manifest.json) and connector/provider engines (one per
declared connection, listed in engines/manifest.json). Surfaces travel as
receipts; appearance travels as appearance.json.
Each blocks/manifest.json entry is a DynamicBlockDescriptor:
{ "blockType": "navbar", "moduleUrl": "blocks/navbar.3f9c1a2b.mjs",
"integrity": "<sha256 hex>", "publisher": "phiwebs",
"publisherVerified": true, "definition": { "name": "navbar" } }Before evaluating a bundle the renderer fetches moduleUrl, hashes the bytes
with SHA-256 and compares them to integrity (hex, or SRI sha256-<base64>).
A missing or mismatching hash is a hard failure. Bundles keep react, zod
and @phimajor-solutions/* external; the shell’s import map points those
specifiers at ./shims/*.mjs.
engines/manifest.json entries are either bundled (moduleUrl +
integrity, same rule as blocks) or declared (an engine object and no
module — the runtime interprets it as data). Never both.
Source: pwfabric-core/packages/contracts/src/fabric/atom.ts (ATOM_KINDS, classifyAtom),
pwfabric-core/packages/contracts/src/fabric/dynamic-artifact.ts (DynamicBlockDescriptor),
pwfabric-core/packages/runtime/src/render/dynamic-block-loader.ts (verifyIntegrity),
types.ts (PwappEngineDescriptor), web-app-assembly-service.ts (BlockManifestEntry, EngineManifestEntry).
The other envelope files
navigation.json—{ navbar: { links: [{ label, href }] } }. A navbar block whoseprops.linksis empty is filled from here (hrefs rewritten); one that already has links is left as authored. A project with no authored navigation gets one derived link per page,href: "/<key>".appearance.json— the appearance config ({ theme, styleVariables, darkTheme, … }). The runner wraps a bare config as{ config }and passes it as the renderer’s appearance override; an object that already hasconfigpasses through; anything else (including the legacy{ use: 'default' }) is ignored and each receipt’s ownappearanceapplies.connections.json—{ connections: [{ name, connector?, config? }] }.nameis what engine entries bind to. Credential-like keys are never written.auth.json— only when the app has a door.providerisoauth2-pkce(issuer,clientId,scopes) orpassword(loginUrl,registerUrl,logoutUrl,sessionUrl, optionalproviders[]). See Visitor sign-in.
Source: helpers.ts (assembleNav), run-app.ts (projectAppearanceRef),
types.ts (PwappNavigation, PwappConnections, PwappAuth),
web-app-assembly-service.ts (deriveNavigation, CREDENTIAL_KEY_RE),
phico-project-service.ts (buildAuthJson).
From the download to runApp
The exported zip contains its shell; serving the unzipped folder is the whole
self-host. index.html is, in essence:
<link rel="stylesheet" href="./assets/theme.css" />
<script type="importmap">{ "imports": { "react": "./shims/react.mjs", "...": "..." } }</script>
<script src="./assets/embed.js"></script>
<div id="root"></div>
<script type="module">
import { runApp } from './assets/runner.js'
runApp({ source: './pwpack', blocks: './blocks/', router: 'hash' })
</script>runApp reads the envelope from source, mounts into mount, resolves the
router, and per route: fetches the receipt, rewrites hrefs, fills the navbar,
sets the title, and calls window.PhiWebs.render(...) in fat mode with the
block manifest and appearance. The renderer is embed.js; if window.PhiWebs
is missing the runner throws. When the app declares sign-in, receipts are
fetched with an x-app-session header; a plain static host ignores it.
Starting from the .pwpack.tgz instead, you must supply what the zip ships:
embed.js, runner.js, the shims and import map, and a blocks/manifest.json
pointing at bundles you host. No engine package produces those from a bare tgz.
Source: run-app.ts (runApp), helpers.ts (fetchJson),
web-app-assembly-service.ts (buildShellHtml, buildImportMapJson).
Versioning
project.json.schemaVersionis"1.0.0"in every envelope the platform writes today. The runner types itstring | numberand does not branch on it.- There is no contracts-version field in the envelope. Compatibility is
carried by the copies inside the zip:
assets/runner.jsandassets/embed.jsare whatever@phimajor-solutions/pwfabric-runner/pwfabric-embedthe exporting server has installed (both4.4.1in current engine source), and block bundles are pinned by hash. - The one compatibility rule in code is the legacy fallback: no
surfaceReceiptFiles⇒surfaces/<key>.receipt.jsonfor every key.
Source: types.ts (PwappProject.schemaVersion), helpers.ts (receiptFilesOf),
pwfabric-core/packages/runner/package.json, pwfabric-core/packages/embed/package.json,
web-app-assembly-service.ts and phico-project-service.ts (schemaVersion: '1.0.0').
Worked example
A two-page app. pwpack/project.json:
{
"schemaVersion": "1.0.0",
"id": "cadence",
"title": "Cadence",
"entrySurfaceId": "home",
"surfaceOrder": ["home", "people"],
"surfaceReceiptFiles": { "home": "surfaces/home.receipt.json", "people": "surfaces/people.receipt.json" },
"identity": { "required": false, "topology": "platform", "methods": [] }
}pwpack/navigation.json is { "navbar": { "links": [ { "label": "Home", "href": "/" }, { "label": "People", "href": "/people" } ] } }.
pwpack/surfaces/home.receipt.json:
{
"title": "Home",
"blocks": [
{ "id": "nav", "type": "navbar", "props": { "links": [], "logoText": "Cadence", "showCTA": false } },
{ "id": "hero", "type": "hero", "props": { "title": "Cadence", "ctaHref": "/people" } }
],
"appearance": { "config": { "theme": { } } },
"capabilities": []
}blocks/manifest.json lists navbar and hero as in the descriptor above.
Served in hash mode: #/people renders people.receipt.json; the navbar’s
empty links are filled as #/ and #/people; the hero’s ctaHref becomes
#/people; #/nowhere renders the not-found page.
Not the same thing: the pack source tgz
the phiwebs-basepack'sscripts/export-tgz.mjs“ (the pack’s pnpm build)
produces a pwpack source archive: atom source trees (blocks/,
capabilities/, appearance/, patterns/, surfaces/, icons/,
connectors/, providers/, auth/) plus an identity-only package.json,
no root manifest. It is the input to marketplace publishing, where the server
classifies each path into an atom kind (classifyAtom in atom.ts) and
bundles it. It is not an app envelope; the runner cannot run it.
Unverified
- The exported
index.htmlis written with<html lang="tr">regardless of the app’s locale; whether that is intended is not stated in code. - The token keys inside
appearance.config.themeareSurfaceThemein contracts; not enumerated here, and the example leaves the object empty. - Whether a
.pwpack.tgzcan be re-imported into PhiWebs was not traced. - On PhiWebs the runner host is
/api/installed/<world>/<project>/, which serves the zip contents with an injected<base href>; that injection was not read. The PhiCo/s/route renders one surface with the platform runtime and does not use the runner or the envelope. - How the runner absolutizes
media/<file>refs againstsourcewas not traced. - No
build_pwappsscript exists in any workspace repository (searched); a local-runner recipe by that name is not part of the published engine.