Skip to content

Latest commit

 

History

History
140 lines (109 loc) · 7.87 KB

File metadata and controls

140 lines (109 loc) · 7.87 KB

Deploying to DigitalOcean App Platform

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

What gets deployed

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.

Verify the build locally first

npm install
npm run build:static     # compiles + assembles ./build
npm run serve:static     # serves ./build at http://localhost:3456

Open http://localhost:3456 — the DAW should boot exactly like npm run dev.

Deploy

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

Option A — DO console (no CLI)

  1. DO console → Apps → Create App.
  2. Choose GitHub → pick spacedevin/deckard → branch main.
  3. When detected, either accept the auto-detected Static Site or click Edit your App Spec and paste .do/app.yaml.
  4. Confirm: Build command npm run build:static, Output directory build.
  5. Create. Subsequent pushes to main auto-deploy (deploy_on_push).

Option B — doctl (CLI)

doctl apps create --spec .do/app.yaml
# later updates:
doctl apps update <APP_ID> --spec .do/app.yaml

How the build resolves the tish compiler

npm 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/tish and the build fails with no tish), and it no longer reads pnpm.onlyBuiltDependencies from package.json. Fix: package.json pins "packageManager": "pnpm@9.15.4" — pnpm 9 reads pnpm.onlyBuiltDependencies from package.json and runs the @tishlang/tish postinstall. The lockfile is lockfileVersion 9.0 (pnpm-9 native), so the pin is fully compatible. There is no pnpm-workspace.yaml — pnpm 9 treats that file as a monorepo definition and errors packages field missing or empty if it has no packages: key (this is not a workspace), so the allow-list lives only in package.json.
  • engines.node is pinned to 22.x (the Active LTS) rather than a wide >=20 range, 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.

No special headers needed

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.


Co-DJ multiplayer gateway (the gateway service)

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 to CODJ_HUB_PORT, then 35987 locally) 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 with missing 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/tish only to fetch the compiler, then tish 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 default rust. cranelift embeds bytecode + the VM runtime, so it sidesteps a rust-backend codegen bug (E0382 move-of-reused-let, filed as tishlang/tish#328). Both are tish build AOT; only cranelift compiles this gateway cleanly today. (Revisit the default backend once #328 is fixed.)
  • Health check is TCP, not HTTP. The gateway has no HTTP layer, so .do/app.yaml omits http_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 at wss://<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 works

Keep the @tishlang/tish@<version> pin in services/gateway/Dockerfile in sync with package.json when 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.

Optional: the LLM co-DJ agent

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.