Architecture
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.
Pipeline
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.
Pull-request orchestration
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.
Packages
internal/compose — the Compose model
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.
internal/analyzer — analysis
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.
internal/generator — static demo generation
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 embedded runtime build chain
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 demoThe 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.
Other web surfaces
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.
Runtime behavior
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.
Extension points: adding an adapter
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.