Deckard is a client-side DAW — it compiles to a static bundle that runs entirely in the browser and works offline. The cheapest, simplest way to host it is App Platform's Static Site component (CDN-served, free tier eligible). The optional co-DJ multiplayer gateway is a separate service (see the bottom).
npm run build:static produces a self-contained ./build directory:
build/
index.html
styles.css
dist/bundle.js # the compiled Tish app (tish build)
scope-worker.js # waveform/meter worker
clock-worklet.js # transport clock AudioWorklet
scratch-worklet.js # turntable scratch AudioWorklet
sync-worklet.js
mespeak-worker.js # TTS vocal worker
mespeak/ # mespeak engine + voices (TTS)
docs/ # human docs → https://deckard.lol/docs/
llms.txt # llmstxt.org index
llms-full.txt # concatenated docs for agents
Docs are generated by npm run build:docs (chained into build:static) from content/docs/ + docs/*.md.
These mirror the absolute paths the app loads at runtime (/scope-worker.js, /mespeak/mespeak.js, …), so the
folder can be served as-is by any static host. The build script is scripts/build-static.mjs.
npm install
npm run build:static # compiles + assembles ./build
npm run serve:static # serves ./build at http://localhost:3456Open http://localhost:3456 — the DAW should boot exactly like npm run dev.
Prereqs: the repo is on GitHub (spacedevin/deckard, branch main) and the DigitalOcean GitHub app is
authorized for it (you'll be prompted on first deploy, or install it from the DO console → Settings → Integrations).
- DO console → Apps → Create App.
- Choose GitHub → pick
spacedevin/deckard→ branchmain. - When detected, either accept the auto-detected Static Site or click Edit your App Spec and paste
.do/app.yaml. - Confirm: Build command
npm run build:static, Output directorybuild. - Create. Subsequent pushes to
mainauto-deploy (deploy_on_push).
doctl apps create --spec .do/app.yaml
# later updates:
doctl apps update <APP_ID> --spec .do/app.yamlnpm run build shells out to tish (the Tish compiler). That binary is installed by the postinstall script
of the @tishlang/tish dependency, which downloads the right binary for the build host's OS/arch — so the
install step MUST be allowed to run that script.
- npm runs postinstall automatically — nothing to configure.
- pnpm v10+ made dependency build scripts opt-in (it prints
ERR_PNPM_IGNORED_BUILDS: @tishlang/tishand the build fails with notish), and it no longer readspnpm.onlyBuiltDependenciesfrompackage.json. Fix:package.jsonpins"packageManager": "pnpm@9.15.4"— pnpm 9 readspnpm.onlyBuiltDependenciesfrompackage.jsonand runs the@tishlang/tishpostinstall. The lockfile islockfileVersion 9.0(pnpm-9 native), so the pin is fully compatible. There is nopnpm-workspace.yaml— pnpm 9 treats that file as a monorepo definition and errorspackages field missing or emptyif it has nopackages:key (this is not a workspace), so the allow-list lives only inpackage.json. engines.nodeis pinned to22.x(the Active LTS) rather than a wide>=20range, which the Heroku/DO Node buildpack flags as a "dangerous range".
If a build ever fails with tish: not found or ERR_PNPM_IGNORED_BUILDS, the install skipped the postinstall —
confirm the packageManager pin is present, or switch the App Platform build to npm.
The app does not use SharedArrayBuffer, so it does not require cross-origin isolation (COOP/COEP). A plain
static host is sufficient. Fonts load from Google Fonts over HTTPS.
Solo/offline play needs nothing else, but .do/app.yaml now also ships the multiplayer
WebSocket hub as a second component so the Co-DJ panel works live. It is pure Tish (no Node or JS), AOT-
compiled to a native binary and shipped in distroless — exactly the
tishlang/tish container-example flow
(compile .tish → Linux glibc binary → distroless/cc):
services/gateway/main.tish— the WS hub (rooms, fan-out, host election). It listens on$PORT(App Platform injects it; falls back toCODJ_HUB_PORT, then35987locally) and speaks WebSocket only. Strict equality throughout — the native backend rejects loose==/!=(and it's banned anyway).services/gateway/Dockerfile— a 2-stage build → a ~20 MB distroless image (binary only):- build stage
rust:bookworm— provides a current cargo + C toolchain. tish's native backends compile a cargo project, so a recent Rust is required (the distro's older cargo fails withmissing field 'when'). bookworm's glibc (2.36) matches the distroless/cc-debian12 runtime — build on anything newer and the binary needs GLIBC_2.38+ and won't load there.npm installs@tishlang/tishonly to fetch the compiler, thentish build --target native --native-backend cranelift main.tish -o gateway. - runtime stage
gcr.io/distroless/cc-debian12— just the compiled binary (links only glibc/libgcc/libm). No shell, node, cargo, or package manager. - Backend =
cranelift, not the defaultrust. cranelift embeds bytecode + the VM runtime, so it sidesteps arust-backend codegen bug (E0382 move-of-reused-let, filed as tishlang/tish#328). Both aretish buildAOT; only cranelift compiles this gateway cleanly today. (Revisit the default backend once #328 is fixed.)
- build stage
- Health check is TCP, not HTTP. The gateway has no HTTP layer, so
.do/app.yamlomitshttp_path— App Platform then does a TCP check on the port, and the open WS listener satisfies it. No HTTP shim needed. - Ingress routes
/codj→ gateway and everything else → web, so the browser connects same-origin atwss://<app-domain>/codj— exactly the default the Co-DJ panel computes (defaultWsUrl()in src/ui/CoDjPanel.tish); the WS field can still override it.
Verify the image locally (matches what App Platform builds — linux/amd64, distroless). The cargo build of the
cranelift runtime takes a few minutes (and is much slower under Mac emulation):
docker build --platform linux/amd64 -f services/gateway/Dockerfile -t deckard-gw .
docker run --rm -e PORT=8080 -p 35987:8080 deckard-gw # logs: "listening ws://0.0.0.0:8080"
# in another shell: tish run services/agent-worker/test-recv.tish → SUCCESS: cross-process WebSocket worksKeep the
@tishlang/tish@<version>pin inservices/gateway/Dockerfilein sync withpackage.jsonwhen you bump the compiler.
Cost: the gateway is an always-on basic-xxs instance (an hourly charge). If you only want the free static
site, delete the services: block + the /codj ingress rule from .do/app.yaml — solo play is unaffected.
The AI co-DJ (services/agent-worker) is a third process — a worker, not
a web-facing service. It connects to the gateway as a client and reads GRADIENT_MODEL_ACCESS_KEY (DO serverless
inference; falls back to an offline demo patch without a key). To run it, add a workers: component with the same
Dockerfile pattern and CMD = npm run agent (point --hub at the gateway). Not included by default.