PocketStack is a browser-only Compose preview compiler. Its GitHub Action drives a Go analyzer and generator, deploys the static result, and reports the outcome on a pull request. The generated application uses a TypeScript browser runtime bundled into the CLI. There is no server, runner, or Docker daemon behind a preview URL.
Read this page to understand where a behavior belongs, how the embedded runtime is built, or how to add a new adapter without weakening the browser-only contract.
pull_request + compose.yaml + project files
|
v
composite Action (checkout, trusted CLI build, lifecycle/reporting)
|
v
internal/compose (model: types + LoadFile)
|
v
internal/analyzer (adapter detection, readiness, suggestions)
|
v
internal/generator (static demo + embedded runtime)
|
v
index.html + app.js + mock-sw.js + pocketstack.manifest.json + assets/
|
+---------------------> Cloudflare Pages + PR summary/comment
|
v
browser runtime dashboard (in the preview viewer's tab)
The Action metadata lives at action.yml; its lifecycle driver is
scripts/action/pocketstack-preview.mjs; and the CLI entry point is
cmd/pocketstack. The compilation pipeline is: compose (model) → analyzer →
generator → embedded browser runtime.
The composite Action is intentionally self-contained. It checks out the PR,
builds the PocketStack CLI from the selected Action revision, and invokes the
analyzer with --safe-root set to the repository workspace. Ready projects are
generated and deployed to a stable pr-<number> Cloudflare Pages branch.
Partial, blocked, and malformed projects deploy only a static report and fail
the check. A closed PR replaces the stable alias with a tombstone.
The orchestration layer owns GitHub summaries, sticky comments, Action outputs, fork/Dependabot restrictions, and Cloudflare deployment. It never starts Docker or runs project scripts in CI. Paths supplied by the PR—Compose, bind mounts, environment files, and adapter assets—must stay inside the checkout and may not escape through symlinks.
Owns the Compose file model: the service/project types and LoadFile, which
reads and parses a single Compose file into those types. It is parsing and data
structures only — no adapter logic or browser concerns.
Reads the Compose model and classifies each service against the adapter
registry. It owns the analysis types (Analysis, ServiceAnalysis,
AssetAnalysis, Readiness, HostRequirements) and the AnalyzeFile /
Analyze entry points.
A service is either browser-native (mapped to an adapter) or unsupported; there is no server-fallback state. This is intentionally conservative — a false positive would create a misleading demo, so adapters are allowlisted and unsupported services keep concrete reasons in the analysis output. The analyzer also produces a readiness score, per-service suggestions, and project next steps so unsupported stacks still get a useful conversion plan.
Implemented adapters:
static-web— copies document-root files fromnginx,httpd, orcaddybind mounts; warns about server config static hosting cannot emulate.frontend— packages a Node/Bun project for WebContainer-style browser execution, including env handling, package-manager detection, and bridge support for PocketStack mock/database endpoints.wasi— packages an explicitly labeled prebuilt.wasmmodule with browser WASI preview imports and a Wasmer JS fallback.mock-http— turns OpenAPI YAML/JSON and JSON fixtures into service-worker HTTP routes.postgres-pglite— maps supported Postgres demos to PGlite with SQL bootstrap, IndexedDB or memory persistence, reset, and query-bridge support.sqlite— runs SQL init/seed assets or seed databases through sql.js in the browser.
See the adapter matrix for how each is selected and the conversion guide for reshaping unsupported services. New adapter behavior is decided here first: the analyzer must be able to explain why a service is supported, what files to copy, what host requirements apply, and what warnings the demo should show.
pocketstack demo runs the analyzer, and only succeeds when every service is
browser-native. If any service is unsupported, it exits with a reasoned
incompatibility report instead of falling back to a server.
When generation succeeds, it writes a static directory:
index.htmlapp.js(the embedded browser runtime)mock-sw.js(the mock service worker)pocketstack.manifest.json- copied assets under
assets/<service>/ - host-config files when cross-origin isolation is required
The generator makes demos portable: assets are copied into deterministic
service folders, root-relative static-site references are rewritten so copied
sites work from a nested path, and host-config files are emitted when an adapter
requires cross-origin isolation. The manifest is version 2, sets
browserOnly: true, and carries adapter metadata, readiness, copied asset
paths, warnings, host requirements, and a stable storage namespace for browser
databases. See the manifest reference.
The browser runtime is TypeScript that lives in web/runtime. It is bundled by
esbuild and embedded into the generator binary, so a released CLI is
self-contained and writes the same runtime into every demo.
web/runtime/src/app.ts
| esbuild --bundle --format=esm --target=es2022
v
internal/generator/runtime/app.js
| //go:embed runtime/*
v
embedded into the pocketstack binary
| generator writes it next to index.html
v
app.js in every generated demo
The bundle command (from package.json) is:
npm run build:runtime
# esbuild web/runtime/src/app.ts --bundle --format=esm --target=es2022 \
# --outfile=internal/generator/runtime/app.jsinternal/generator/runtime/ also holds mock-sw.js, the mock service worker.
The generator embeds the whole runtime/ directory with //go:embed runtime/*
and copies each file into the output during generation.
::: warning
Editing web/runtime/src/*.ts does not change generated demos until you run
npm run build:runtime to regenerate internal/generator/runtime/app.js. The
Go embed reads the built artifact, not the TypeScript source. CI and
make release-check rebuild it before testing.
:::
Two web apps are built from the same repo but are not embedded in the CLI:
web/studio— the in-browser Studio analyzer (paste/upload a Compose file for browser-only triage).web/site— the landing page.
Three production-quality applications under examples/showcase/ exercise the
same generator and browser adapters used by the Action. The maintainer showcase
workflow deploys them to stable Cloudflare Pages branches.
These ship to the public GitHub Pages site alongside selected generated demos.
Generated demos run as static browser code:
static-webservices render as iframe previews;frontendservices boot in a WebContainer-style runtime;mock-httpservices register routes inmock-sw.js;- database services initialize PGlite or SQLite on demand;
wasiservices execute prebuilt WebAssembly modules;- logs, status, warnings, previews, reset controls, and query panels live in the generated dashboard.
The runtime may load public browser packages or npm dependencies when an adapter requires them, but it never calls a PocketStack backend. Custom UI can drive the demo through browser-only service URLs.
A new adapter starts in internal/analyzer and flows through the pipeline:
- Analyzer — add classification: detect the service, declare which files to
copy (as
AssetAnalysis), setHostRequirements, and emit clearwarningsandunsupportedreasons for nearby cases that still cannot work. - Generator — ensure the copied assets and
configkeys the runtime needs land in the manifest (see howcopyServiceAssetsmaps asset names to config ininternal/generator/generator.go). - Runtime — implement the browser behavior in
web/runtime/srcand rebuild withnpm run build:runtime. - Tests & docs — analyzer classification tests, manifest coverage, runtime
tests, an example Compose project, and an adapter page under
/adapters/.
Keep new behavior adapter-shaped. If a feature needs Docker networking, privileged filesystem access, or a long-running server that is not a browser primitive, it belongs in the unsupported report until there is a real browser implementation. The full checklist is in the contributing guide.