Pulsegrid: build and ship a Go WebAssembly game with app.nz CI
A complete open-source reference: deterministic Go puzzle rules, a tiny WASM bridge, portable app.yaml jobs with hybrid caches, Visualbench checks, and a static app.nz subdomain.
Listen to this article
On-device voiceUses the voice built into your browser; no article text leaves this page.
Audio narration is not supported by this browser.
Pulsegrid began as a practical question: can a tiny open-source game exercise the same build, cache, artifact, hosting, and release path that a real Go application uses—without hiding the interesting parts behind a framework?
The result is playable at pulsegrid.app.nz. It is a daily circuit puzzle: rotate cable tiles until a center reactor reaches four sleeping beacons. The board generator, tile rotation, graph traversal, and win state are Go compiled to WebAssembly. JavaScript renders the snapshot and adds sound; it does not reimplement the rules.
The complete source is public on app.nz, with an MIT license, native Go tests, portable app.yaml, a static hosting manifest, and reproducible desktop/mobile Visualbench captures.
Why this architecture is a useful CI example
A browser game can easily become a JavaScript project with a ceremonial Go binary. Pulsegrid takes the opposite approach:
seed
│
▼
Go spanning-tree generator
│
├── native go test
└── GOOS=js GOARCH=wasm
│
▼
compact JSON snapshot
│
▼
accessible HTML/CSS boardThe core package does not import syscall/js, so normal CPU tests cover the same rules shipped to the browser. Only a small build-tagged bridge knows about WebAssembly.
A guaranteed-solvable daily board
The generator starts at the center and builds a randomized depth-first spanning tree over all 25 cells. Each edge becomes reciprocal cable bits on neighboring tiles. The solved tree therefore reaches every cell and cannot contain an isolated island.
Four distant leaves become beacons. The engine then rotates each tile using the same seeded random source. A date such as 20260723 always recreates the same puzzle, which is handy for both a daily challenge and deterministic regression tests.
After each turn, a breadth-first traversal follows only reciprocal connections: an east cable is live only if its neighbor exposes west. The puzzle is complete when the traversal reaches all four beacon indices.
One portable app.yaml
The repository uses the app.nz CI contract directly:
version: 1
ci:
name: Pulsegrid
machine:
kind: cpu
cpu: 3
memoryGb: 4
providers: [appnz, hetzner]
prefer: [appnz-shared, cpx21, cpx32]
maxPrice: 0.02
fallback: true
cache:
mode: auto
backend: hybrid
namespace: pulsegrid-go
jobs:
test:
image: golang:1.25-bookworm
steps:
- run: go test ./...
- run: go vet ./...
wasm:
needs: [test]
image: golang:1.25-bookworm
steps:
- run: ./scripts/build.shThe job asks for capabilities rather than hard-coding one runner. app.nz can use the shared pool first and fall back to a compatible Hetzner CPU under the price ceiling. Automatic hybrid caching recognizes go.mod and keeps module/build caches hot locally with a private R2 snapshot when configured.
This is intentionally modest infrastructure. A 3-vCPU test should not occupy a GPU, and a small open-source project should not need a Kubernetes vocabulary.
CI and hosting remain separate
app.yaml builds and verifies the artifact. appnz.yaml says how to publish it:
name: pulsegrid
runtime: static
output: distThe build script copies the static shell and Go's matching wasm_exec.js, then creates a trimmed WebAssembly binary:
GOOS=js GOARCH=wasm go build -buildvcs=false -trimpath -ldflags="-s -w" \
-o dist/pulsegrid.wasm ./cmd/wasm-buildvcs=false also makes the build portable to archive and bare-repository checkouts where Go cannot inspect normal worktree metadata. Keeping the deploy directory explicit makes a release auditable: only the files in dist/ reach the subdomain.
Reproduce it
git clone https://app.nz/leepenkman/pulsegrid.git
cd pulsegrid
./scripts/test.sh
python3 -m http.server 4173 --directory dist
# Once signed in:
app ci create --name Pulsegrid --repo leepenkman/pulsegrid \
--provider appnz --config-file app.yaml
app apps deploy .The detailed step-by-step tutorial covers the engine boundary, WASM bridge, cache choices, deployment, and production checks.
The important result is not merely that the game loads. The same source passes native tests, compiles for the browser, is inspected at desktop and phone widths, publishes through the CLI, receives its own HTTPS subdomain, and appears in the app.nz marketplace. That is the path an agent-generated example should prove.