workbooks docs

wbx deploy & the OCI image

Deploy Kit is the USER tool: it runs the one runtime OCI image for your own use — locally (a container) or in the cloud (a provider recipe). It is NOT how the platform publishes the compilers package or the runtime image. The bootstrap verb is special: it must run with NO runtime up — it is what brings the runtime up, reading deployment.org and converging via container engine / provider recipe. Native-only.

The three release layers must not be conflated. Deploy Kit is layer 3 (users only). The compilers package (layer 1, published manually) and the runtime image (layer 2, built by CI) are NOT wbx deploy. See Release layers.

Verbs

The DeployVerb enum (clap subcommands) — cli/src/deploy/mod.rs:31.

verbdoessrc
wbx deploy init [preset]scaffold ./deployment.org; preset local | cloud (TTY picks)cli/src/deploy/mod.rs:34,161
wbx deploy validatecoherence check on deployment.org incl. declared secretscli/src/deploy/mod.rs:40,152
wbx deploy applyconverge to declared state (local container or provider recipe)cli/src/deploy/mod.rs:42,363
wbx deploy statusinspect the live deploymentcli/src/deploy/mod.rs:44,98
wbx deploy logstail the live deployment's logscli/src/deploy/mod.rs:46,99
wbx deploy downtear it downcli/src/deploy/mod.rs:48,100
wbx deploy localshorthand: scaffold local config if absent, then applycli/src/deploy/mod.rs:50,101
wbx deploy doctorengines, recipes, declared secrets — present vs missingcli/src/deploy/mod.rs:52,527
wbx deploy secrets …manage kit-abstracted, provider-delivered secrets (subcommands)cli/src/deploy/mod.rs:54,223

Note: the binary is invoked wbx (cli/src/main.rs:24, name = "wbx"). Older docs and source moduledoc comments still say wb deploy; the shipped command is wbx deploy.

deploy secrets subcommands

verbdoessrc
wbx deploy secrets set KEY=VALUE …stage KEY=VALUE pairs (or --from-env <file>)cli/src/deploy/mod.rs:61,225
wbx deploy secrets listlist secret NAMES (never values)cli/src/deploy/mod.rs:65,251
wbx deploy secrets unset KEYremove a secret by namecli/src/deploy/mod.rs:67,258
wbx deploy secrets pushpush staged secrets to the provider without a deploycli/src/deploy/mod.rs:71,266

Secrets are declared by NAME in deployment.org (#+DEPLOY_SECRETS: KEY …), values staged via secrets set, stored 0600 at <app-dir>/secrets.env (cli/src/deploy/mod.rs:187,206-211), and delivered per provider — env-file locally, the recipe's provider_set_secrets hook in the cloud. Values never enter deployment.org or images (cli/src/deploy/mod.rs:18-21).

deployment.org — the shipped (CLI) keyword schema

The shipped wbx deploy parses deployment.org as flat #+KEYWORD: lines (org_keywords, cli/src/deploy/mod.rs:149). Recognized keys:

keyworddefaultmeaningsrc
#+DEPLOY_TARGET:local (alias PLACE:)local or a provider recipe name (e.g. fly)cli/src/deploy/mod.rs:117
#+DEPLOY_APP:workbooks (alias APP:)app / container namecli/src/deploy/mod.rs:120
#+DEPLOY_REGION:sjccloud regioncli/src/deploy/mod.rs:123
#+DEPLOY_IMAGE:ghcr.io/workbooks-sh/runtime:latestOCI image (WB_IMAGE env overrides)cli/src/deploy/mod.rs:126,76
#+DEPLOY_PORT:4000host + container portcli/src/deploy/mod.rs:132
#+DEPLOY_SECRETS:(none)space-separated secret NAMES the deploy needscli/src/deploy/mod.rs:136
#+DEPLOY_TOOLKITS:(none)id:dir id:dir … pushed after a cloud applycli/src/deploy/mod.rs:384

WB_IMAGE (env) always wins over #+DEPLOY_IMAGE: = (=cli/src/deploy/mod.rs:127-131). The default image constant is ghcr.io/workbooks-sh/runtime:latest (cli/src/deploy/mod.rs:76).

Scaffold templates

  • init local writes TEMPLATE_LOCAL (#+DEPLOY_TARGET: local, port 4000, OPENROUTER_API_KEY secret) — cli/src/deploy/mod.rs:558.

  • init cloud / init fly writes TEMPLATE_FLY (#+DEPLOY_TARGET: fly, #+DEPLOY_REGION: sjc) — cli/src/deploy/mod.rs:570.

The ONE OCI image

There is ONE artifact: ghcr.io/workbooks-sh/runtime:<tag> — the BEAM runtime + wasmtime + litestream + the release, with the in-sandbox compilers pulled in as a separate layer (COPY --from). Every place runs the same image; a target is just a routing config + endpoint, not a code fork.

factvaluesrc
registryghcr.io/workbooks-shruntime/host/deploy/image.ex:11
image nameruntimeruntime/host/deploy/image.ex:12
canonical refghcr.io/workbooks-sh/runtime:latestruntime/host/deploy/image.ex:21
override envWB_IMAGEruntime/host/deploy/image.ex:21
Dockerfileci/Dockerfile.runtimeruntime/host/deploy/image.ex:13
compilers layer refghcr.io/workbooks-sh/compilers:latestruntime/host/deploy/image.ex:24
compilers envWB_COMPILERS_IMAGEruntime/host/deploy/image.ex:24

Local converge

#+DEPLOY_TARGET: local runs the image in a container on this machine. The engine is auto-detected in this preference order: docker, podman, krunvm (cli/src/deploy/mod.rs:359-361); first one with a working --version wins.

enginemechanismsrc
docker=/=podmanpull then run -d --name <app> -p <port>:<port> with env + volumescli/src/deploy/mod.rs:445
krunvmcreate <image> --cpus 2 --mem 2048 then detached startcli/src/deploy/mod.rs:469

Mounted volumes + env on the container (cli/src/deploy/mod.rs:436-454): <app-dir>/disco → /disco (discovery, WB_DESKTOP_DIR), <app-dir>/data → /data (WB_DATA); env WB_WEB=1, WB_DESKTOP=1, PORT=<port>. After apply, talk to it with wb rt status (cli/src/deploy/mod.rs:464).

krunvm limitation: krunvm cannot deliver the kit's secrets env-file yet — apply bails if secrets are staged. Use docker/podman locally, or a cloud provider. (cli/src/deploy/mod.rs:470-472)

Re-applies are idempotent: docker/podman rm -f the prior container, krunvm delete=s the prior VM, before recreating (=cli/src/deploy/mod.rs:447,473).

Cloud converge (provider recipes)

Any #+DEPLOY_TARGET: other than local names a provider RECIPE at providers/<place>/bootstrap.sh, which sources the neutral spine _recipe.sh and fills five hooks. Adding a provider = dropping a bootstrap.sh — no recompile (cli/src/deploy/mod.rs:5-11).

Recipe resolution order (cli/src/deploy/mod.rs:279-298):

  1. $WB_PROVIDERS_DIR

  2. cli/deploy-kit/providers (repo dev, walking up)

  3. recipes EMBEDDED in the binary, materialized under the app dir (standalone).

Recipe hooks (the provider contract)

A bootstrap.sh sets WB_RECIPE_PLACE, fills these, and calls wb_recipe_run.

hookrolesrc
provider_ensure_appcreate the app/hostcli/deploy-kit/providers/_recipe.sh:57
provider_set_secretsstage credentialscli/deploy-kit/providers/_recipe.sh:58
provider_attach_volumedurable volume for the data dircli/deploy-kit/providers/_recipe.sh:59
provider_deploy_imagerun the OCI imagecli/deploy-kit/providers/_recipe.sh:60
provider_public_urlthe engine's URLcli/deploy-kit/providers/_recipe.sh:61
provider_down/status/logslifecycle triocli/deploy-kit/providers/_recipe.sh:69-71

Actions dispatched via WB_RECIPE_ACTION (up | down | status | logs | url | secrets) — cli/deploy-kit/providers/_recipe.sh:53. The engine env every provider forwards is assembled by wb_base_env (WB_WEB=1, PORT, WB_DATA, WB_REGISTRY, plus tenancy / storage / db / secret keys) — cli/deploy-kit/providers/_recipe.sh:33-46.

Bundled provider: fly

Fly is the one bundled recipe (personal preference, not privileged — cli/deploy-kit/providers/fly/bootstrap.sh:2-3). Requires fly / flyctl on PATH (:19-21). Deploys either a prebuilt WB_IMAGE or, if WB_FLY_CONFIG / WB_FLY_DOCKERFILE set, a remote build (:52-82). Public URL https://<app>.fly.dev (:88). Lifecycle: fly apps destroy --yes / fly status / fly logs (:90-92). Fly-specific env: WB_FLY_APP, WB_FLY_CONFIG, WB_FLY_DOCKERFILE, WB_INCLUDE_CLIP (:13-14).

Cloud bearer token

On the FIRST cloud apply, the kit generates a 256-bit random WB_PUBLIC_BEARER, persists it in secrets.env, and delivers it to the engine — the control plane rejects requests without it. It is generated ONCE and never rotated (rotation would 401 all live clients). Set WB_ENGINE_TOKEN=<token> to call the engine (cli/src/deploy/mod.rs:406-422).

Environment variables (deploy-side)

envrolesrc
WB_IMAGEoverride the OCI image refcli/src/deploy/mod.rs:127
WB_PROVIDERS_DIRexplicit providers directorycli/src/deploy/mod.rs:280
WB_RECIPE_ACTIONpassed to recipe (up/down/status/logs/url/secrets)cli/src/deploy/mod.rs:327
WB_APP_NAME / WB_PORT / WB_REGIONforwarded to recipe from configcli/src/deploy/mod.rs:330-332
WB_SECRET_KEYSspace-separated names the recipe forwardscli/src/deploy/mod.rs:333
WB_PUBLIC_BEARERcloud control-plane bearer (auto-generated)cli/src/deploy/mod.rs:408
WB_ENGINE_TOKENclient token to call a locked enginecli/src/deploy/mod.rs:418
WB_FLY_APP/CONFIG/DOCKERFILE, WB_INCLUDE_CLIP, WB_EMBEDpassthrough overrides to recipescli/src/deploy/mod.rs:342

OQL kernel (WIT world)

The OQL kernel (oql.wasm) is a typed WebAssembly Component — pure in / pure out, every export string -> string, no host imports, so it instantiates with no WASI context and any fault traps inside Wasmtime (runtime/kernel/wit/world.wit:6-10). Package workbooks:oql, world oql.

exportsignaturedoessrc
parse-headlinesfunc(org: string) -> stringOrg structure → headline rows (JSON)runtime/kernel/wit/world.wit:13
lintfunc(org: string) -> stringOrg → diagnostics (JSON)runtime/kernel/wit/world.wit:15
tangle-planfunc(org: string) -> stringliterate Workbook → WIT-world build plan (JSON)runtime/kernel/wit/world.wit:17
validatefunc(org: string) -> stringbuild plan → diagnosticsruntime/kernel/wit/world.wit:19
check-upgradefunc(old: string, new: string) -> stringdiff deployed vs new world, refuse breakingruntime/kernel/wit/world.wit:21
renderfunc(org: string) -> stringOrg → rich HTMLruntime/kernel/wit/world.wit:23

Release layers (do not conflate)

Three distinct layers — Deploy Kit is only layer 3.

layerartifacthow publishedsrc
1 compilers packageghcr.io/workbooks-sh/compilers:{latest,sha}MANUAL, publish_compilers/1; CI cannot build itruntime/host/deploy/image.ex:97
2 runtime imageghcr.io/workbooks-sh/runtime:{latest,sha}CI (runtime-image.yml); COPY --from compilersruntime/host/deploy/image.ex:53
3 wbx deployruns the runtime image (user's registry/host)USERS only — init/validate/apply/…cli/src/deploy/mod.rs:82

Layer-1/2 maintainer functions (NOT CLI verbs; not wbx deploy): Workbooks.Deploy.Image.build/1 (local single-arch, image.ex:31), publish/1 (multi-arch push, image.ex:53), build_compilers/1 (image.ex:80), publish_compilers/1 (image.ex:97).

Divergence: the Elixir backend model (NOT the shipped CLI)

runtime/host/deploy/config.ex and backend.ex implement a richer model parsed from a :PROPERTIES: drawer (config.ex:29-49), used by the deploy-kit example descriptors in cli/deploy-kit/deployments/:

property (drawer)allowedsrc
:ENGINE_PLACE:local | cloudruntime/host/deploy/config.ex:15
:TENANCY_MODE:single | multiruntime/host/deploy/config.ex:16
:STORAGE:local-fs | s3runtime/host/deploy/config.ex:17
:DATABASE:sqlite | postgresruntime/host/deploy/config.ex:18
:AUTH:trusted | betterauth | clerk | oidcruntime/host/deploy/config.ex:19
:PROVIDER:cloud recipe name (default fly)runtime/host/deploy/config.ex:137

Coherence rules it enforces (config.ex:56-83): multi tenancy on sqlite is rejected (one writer serializes tenants); multi needs real AUTH; s3 needs STORAGE_BUCKET + STORAGE_ENDPOINT + WB_S3_KEY=/=WB_S3_SECRET env; postgres needs WB_DATABASE_URL env. Secrets stay in the deploy ENV, never the file (config.ex:6-9). See examples cli/deploy-kit/deployments/local.org and cloud-saas.org.

See also