Quick start
Two things bring people here. Pick the one that is yours.
| You have… | Track |
|---|---|
| an app built on PhiWebs, and a host of your own | A — run it on a static host |
| an idea for a block the catalog does not have | B — write a first block |
Track A — run an exported app on a static host
A pwapp runs on anything that serves files. There is no build step and no Node process: the runner is a browser ES module that reads the app’s JSON files and mounts the renderer.
1. Export the app. In PhiCo choose Export, which gives a zip that is a
complete runnable folder. (Download gives only the .pwpack.tgz envelope,
which has no engine in it and is not runnable by itself.)
2. Unzip it. You get:
index.html ← loads the runner and calls runApp()
assets/ ← runner.js, embed.js, theme.css
shims/ ← import-map targets the block bundles resolve against
pwpack/ ← project.json, surfaces/*.receipt.json, navigation, appearance…
blocks/ ← manifest.json + one hash-pinned bundle per block type
engines/ ← connector engines, if the app has connections3. Serve the folder. Any static file server will do. Open it in a
browser: the runner fetches pwpack/project.json and blocks/manifest.json,
mounts into #root, and routes with #/<page> in the URL — hash routing is
the default because it works on every host without configuration.
The shell the export writes is, in essence:
<link rel="stylesheet" href="./assets/theme.css" />
<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() takes source, blocks, engines, router ('hash' ·
'path' · 'auto'), mount and locale; every one has a default that
matches the folder above, so runApp() with no arguments is equivalent. Clean
URLs (router: 'path') need the host to serve index.html for every path
and a <meta name="pw-router" content="path" /> tag in the shell.
That is the whole self-host for a static app. If the app reads or writes data it will call an API; if it gates pages behind sign-in it will talk to an issuer. What goes on the wire in each case, and what your host must provide, is in Self-hosting: the envelope, data and receipts, visitor sign-in.
Source: pwfabric-core/packages/runner/src/types.ts (RunAppOptions),
pwfabric-core/packages/runner/src/run-app.ts (runApp).
Track B — write a first block
You need Node 22, pnpm 10, and a project that renders React 19 — or an empty package to try this in.
1. Install the authoring package.
pnpm add @phimajor-solutions/pwfabric-authoring zod react2. Write the block. Put it at blocks/customer-quote/component.tsx, the
place a pwpack source tree expects it:
import { defineBlock } from '@phimajor-solutions/pwfabric-authoring'
import { z } from 'zod'
export const CustomerQuote = defineBlock({
type: 'customer-quote',
name: 'Customer quote',
category: 'text',
schema: z.object({
quote: z.string().min(10).max(500),
author: z.string(),
role: z.string().optional(),
}),
defaultProps: {
quote: 'A great product to work with.',
author: 'Jane Doe',
role: 'Product Manager',
},
component: ({ props }) => (
<blockquote>
<p>"{props.quote}"</p>
<footer>{props.author}{props.role ? `, ${props.role}` : ''}</footer>
</blockquote>
),
})type is the block’s identity: 2–50 characters, kebab-case, and not one of
the first-party names. defineBlock() throws on a bad type and warns on
anything else it can work around.
3. Validate it. validateBlockDefinition() is the check the publish gate
runs; validateBlock() checks an instance against your schema.
import {
validateBlockDefinition,
validateBlock,
formatValidationErrors,
} from '@phimajor-solutions/pwfabric-authoring'
import { CustomerQuote } from './blocks/customer-quote/component'
const issues = validateBlockDefinition(CustomerQuote)
if (issues.some((i) => i.severity === 'error')) {
throw new Error(issues.map((i) => `${i.code}: ${i.message}`).join('\n'))
}
const result = validateBlock(
{ id: 'q1', type: 'customer-quote', props: { quote: 'Too short', author: 'A. Reader' } },
CustomerQuote,
)
console.log(result.valid ? 'ok' : formatValidationErrors(result.errors))
// [VALIDATION_ERROR] at quote: String must contain at least 10 character(s)4. See it render. A registry turns definitions into the component map the renderer takes:
import { createCustomBlockRegistry, BlockSurfaceRenderer } from '@phimajor-solutions/pwfabric-authoring'
import { CustomerQuote } from './blocks/customer-quote/component'
const registry = createCustomBlockRegistry()
registry.register(CustomerQuote)
export function Page() {
const surface = {
blocks: [{ id: 'q1', type: 'customer-quote', props: { quote: 'Shipped in an afternoon.', author: 'A. Reader' } }],
}
return <BlockSurfaceRenderer surface={surface} components={registry.getComponents()} />
}5. Ship it. A block reaches other people’s pages as a hash-pinned ES module listed in an app’s block manifest, built from the folder you wrote in step 2. The shapes are in How a block reaches a page.
Source: pwfabric-core/packages/authoring/src/define-block.ts
(defineBlock, validateBlockDefinition),
pwfabric-core/packages/authoring/src/validation.ts (validateBlock, formatValidationErrors),
pwfabric-core/packages/authoring/src/registry.ts (getComponents).
Next steps
- Authoring — the full export surface, and factories and capabilities
defineBlock()— every option, and what the runtime does with each- Self-hosting — everything a downloaded app needs from a host
- Licensing — what you may do with the code