Quickstart

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 ownA — run it on a static host
an idea for a block the catalog does not haveB — 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 connections

3. 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 react

2. 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