Useful Commands

A scenario-based reference for the commands an application developer uses to develop, deploy, and operate their app on the WBSP platform — all through the wbsp CLI — organised by environment:

  • compose / dev / standalone — apps deployed on your machine (whole stack in containers, IDE + data-services only, or a single all-in-one container)
  • aws — apps deployed to the platform EKS cluster
  • aws.on-demand — apps deployed as on-demand AWS Lambda functions
  • aws.demo — throwaway sample-mode instances (Lambda, sample data baked in)
  • universal — publish an appliance-runnable (multi-architecture) image; deploys nothing

Each section is self-contained — commands are repeated where they apply to more than one environment. Operator commands that need direct AWS/cluster access (wbsp-platform, kubectl, aws, terraform) are a separate, operator-only concern and are not covered here.

Deploying and lifecycle. Configs declare a top-level destination: block and deploy with --destination <name> (reading .env.<destination>). Lifecycle commands (status, logs, remove, stop) scope by --enclave <name> instead (auto-detected from deployment state). See the application creator guide and wbsp-yaml-reference.md.

The binaries

Built into bin/ in the repository (rebuild after any Go change — see install-binaries):

BinaryRole
wbspDeploy and manage applications — authenticate (login, logout, whoami, token), deploy & lifecycle (deploy, list, status, logs, stop, remove, services, prepare, dump-data), database (db …), and demos (demo …)
wbsp-platformOperator tooling — provision/destroy infrastructure, platform health, cleanup
wbsp-apiThe platform API server (runs as a platform component)

Conventions used below

  • wbsp reads a .env beside the app's yaml (it never reads platform config).
  • How wbsp authenticates to the platform (login, tokens, or direct AWS) is its own topic — see Credentials next.
  • Naming: an app deploys into namespace wbsp-<tenant>-<enclave>-<app> with workload <tenant>-<app>. The enclave is the isolation group the app was placed in, and it defaults to the tenant name — which is why a namespace often repeats it, as in wbsp-twist-twist-newsletter. A parallel-services environment uses namespace wbsp-svc-<tenant>-<env-name>.

Credentials — three ways to authenticate

wbsp can reach the platform three different ways. As a normal developer you use only the first (wbsp login); the other two exist for CI and for operators.

1. wbsp login — the normal developer path (no AWS keys)

A browser OIDC sign-in against HAP (the platform's auth server). One command sets up everything — the API, git, and container-registry credentials:

wbsp login                         # opens your browser; signs in to tenant `wbsp` (website accounts)
wbsp login --tenant twist          # …or sign in to ONE named tenant
wbsp login --destination twist     # …or read the tenant a destination declares
wbsp login status                  # every tenant you hold a session for
wbsp logout                        # end every session (--tenant <name> for one)

You sign in to a tenant. A session is valid only for the tenant that issued it, so sign in once per tenant you work in — sessions are kept side by side. --tenant names the tenant; --destination reads the tenant: a destination declares in wbsp.yaml. They are not interchangeable: a destination's name is not its tenant (a destination called aws may well declare tenant philcal). With neither flag, you sign in to tenant wbsp — the wbsp.ai website's user namespace, where an account created on the website lives — and the CLI says so, listing any other tenants your project's destinations reach so you can re-run with --tenant if you meant one of those.

  • Caches one API token per tenant under ~/.config/wbsp/tokens/. Every API-path command (deploy, list, status, logs, remove, demo …) sends it to the platform API (https://api.wbsp.ai by default; WBSP_API_URL overrides) — no AWS credentials required.
  • Also registers wbsp as your git + docker credential helper for the platform's hosts, so git push, docker push, and wbsp deploy all work off that one login — each push asks the helper for a freshly-issued token (nothing static is stored in your keychain). The CLI silently renews your session from its refresh token, so you rarely sign in again. For this to work, wbsp must be on your PATH (git and docker invoke it by name).

No configuration is needed against the wbsp.ai platform — the API endpoint, login issuer, and CLI client id are all built-in defaults. Self-hosted platforms only: override them once (values from your operator) in your shell profile:

export WBSP_API_URL=<your wbsp-api endpoint>          # default https://api.wbsp.ai
export WBSP_HAP_BASE_URL=https://<your-hap>           # the provider's base — NO /t/<tenant>

The CLI's sign-in client is built into every HAP tenant, so there is no client id to configure any more (WBSP_CLI_CLIENT_ID is only for a provider that lacks the built-in one).

Being signed in isn't the same as being allowed: the tenant's own administrator grants you the deployer role, in HAP's own screens — no platform operator is involved. A refusal tells you exactly which tenant, which role, and where an administrator of that tenant can grant it. Full walkthrough: HOWTO-bootstrap-developer.md.

2. WBSP_API_TOKEN — one secret for CI / non-interactive

When there's no browser (CI, scripts), supply one bearer instead of wbsp login: WBSP_API_TOKEN, an access token minted by a service account at the identity provider — an application in the tenant the pipeline acts on, created by that tenant's administrator with the scopes the work needs (wbsp-platform:deploy for a pipeline that deploys). The pipeline mints it with the client-credentials grant at the start of a job:

export WBSP_API_TOKEN=$(curl -sS -X POST https://hap.wbsp.ai/t/<tenant>/oauth2/token \
  -d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET" \
  | jq -r .access_token)
wbsp deploy --destination aws-prod

The token serves one tenant (the one in its issuer) and lives an hour by default, so mint per job rather than storing one. Revoke by disabling the application at the provider — effective within one token lifetime.

The platform itself mints nothing (feature 093): wbsp token create and the platform-signed CI tokens it produced are gone. The API accepts a service account's token everywhere a login session is accepted. The registry and git gateways are the website's, and accept what the website configures them for — today that is people's CLI sessions; a pipeline that pushes images with a service-account token needs the website team to admit that audience.

Retired (feature 075): WBSP_REGISTRY_TOKEN and per-developer personal access tokens (PATs) are no longer accepted — setting WBSP_REGISTRY_TOKEN is now a hard error pointing you here. Use WBSP_API_TOKEN (a service account's token) or wbsp login (interactive). SSH keys remain available for plain git access.

Override hosts if your platform uses different domains: WBSP_REGISTRY_HOST, WBSP_GIT_HOST. Full detail: credential-less-deploy.md.

wbsp whoami — who am I, what am I working on, which service?

The one command that answers all three at once. Run it whenever a git push, wbsp deploy, or catalog lookup is rejected and the reason isn't obvious.

wbsp whoami            # just the variant UUID  (script-friendly)
wbsp whoami --user     # just the signed-in account  (script-friendly)
wbsp whoami --all      # every field, labelled  (-v is an alias)
wbsp whoami --json     # every field as parse-able JSON

--all prints nineteen fields in four groups:

user:      philcal@mac.com
sub:       d8f40197-7a4f-45bd-af04-fe6b8f2aebc9
admin:     yes
tenants:   wbsp
source:    hap
issuer:    https://hap.wbsp.ai/t/wbsp
expires:   2026-08-03T17:07:12+08:00 (in 55m)

variant:   64656751-e7f1-43d7-b3aa-f9723170e4d5
name:      core
project:   112-school-information-system
slug:      core
url:       https://wbsp.ai/catalog/school-information-system
recorded:  (not recorded in wbsp.yaml)

owner:     philip-callender-hh70
repo:      112-school-information-system-core
remote:    https://git.wbsp.ai/philip-callender-hh70/112-school-…-core.git

api:       https://api.wbsp.ai
version:   v0.2.18
token:     session

warning: signed in as philcal@mac.com but the repository is owned by philip-callender-hh70

That last warning is the point of the command. The most common baffling 403 on this platform is signing in as one account while the repository belongs to another — the two identifiers look similar enough that nobody spots it by eye. whoami compares them for you. It also warns when the variant: recorded in your wbsp.yaml has drifted from the one your remote actually resolves to.

Warnings never change the exit code — this command reports, it never gates.

Every field is optional. Not signed in, no network, not in a git repository — --all and --json still print everything they can and say why the rest is missing, then exit 0. Only the two single-value forms (bare and --user) fail loudly, so a script never captures an empty string.

For automation, use --json. Its keys are a documented contract; the labelled output is for humans and may change between releases.

wbsp whoami --json | jq -r .user.email
wbsp whoami --json | jq -r '.warnings[] | "\(.field): \(.reason)"'

# The bare form is what you patch into wbsp.yaml:
yq -i ".variant = \"$(wbsp whoami)\"" wbsp.yaml
JSON keyMeaning
user.email / user.subThe account that would be used on the next API call
user.authority / user.kind / user.tenants / user.sourceFrom the platform: the verbs the credential carries in its tenant, whether a person or a service account holds it, the tenant itself
user.issuer / user.expiresWhich HAP tenant issued the session, and when it lapses
variant.idvariant.urlThe catalog variant this repository resolves to
variant.recordedThe variant: currently written in ./wbsp.yaml
repo.owner / repo.name / repo.remoteWhat the git remote points at
environment.apiWhich platform API you are talking to
environment.versionYour wbsp build — put this in bug reports
environment.tokensession (from wbsp login) or env (WBSP_API_TOKEN)
warnings[]{field, reason} — unavailability and mismatches alike

Unavailable fields are null, never omitted, so the document's shape is safe to parse in every state. user.tenants distinguishes null ("unknown") from [] ("member of no tenant").

With WBSP_API_TOKEN set, the identity reported is that token's, not your cached login — because that token is what every call actually presents. environment.token tells you which is in play. Without it, --user answers from the local cache and makes no network call at all.

3. AWS_PROFILE + --backdoor — direct AWS (operators only)

Bypasses wbsp-api and drives AWS directly with your own AWS credentials (build + push to ECR and roll out on EKS yourself). For bootstrap, recovery, and deep debugging — a normal developer never needs this.

export AWS_PROFILE=<profile>                       # your AWS credentials
wbsp deploy --config wbsp.yaml --destination prod --backdoor
  • --backdoor is a persistent flag on any AWS-typed command; it always requires AWS_PROFILE (missing it is an immediate error). Region, account, ECR, and the EKS endpoint are auto-derived from the profile.
  • Without --backdoor, the default (API) path needs neither AWS_PROFILE nor AWS keys — use path 1 or 2.

local-machine types — compose / dev / standalone

Apps run on your own machine. A destination's type: in the app's wbsp.yaml picks one of three deployment shapes:

  • compose — the whole app stack runs in containers (app + a per-app PostgreSQL/Redis), production-parity.
  • dev — ONLY the data services run in containers; you run the app from your IDE (live reload). The deploy writes a .env next to the app with discrete DATABASE_* / REDIS_* values pointing at localhost on derived ports — source it and start your app.
  • standalone — app + DB + Redis inside ONE ephemeral container.

All three share the same contract: deploy with wbsp deploy --destination <name>, expose the same discrete DATABASE_* / REDIS_* env vars as aws (no DATABASE_URL/REDIS_URL), use no Traefik (routes are ignored), and host-publish ports derived from the project number (Postgres 54NNN, Redis 63NNN, the app from its declared port; values above 65535 fold into [49152, 65535]).

One-time setup — the shared Docker network

docker network create wbsp-shared

This network is operator-managed — the compose provider never creates or destroys it. Every compose-deployed app attaches to it for cross-stack reachability (HAP, shared services).

Basics — deploy, list, status, remove

# Deploy / redeploy an app (the destination's type selects compose/dev/standalone)
wbsp deploy --config path/to/wbsp.yaml --destination dev

# Deploy AND write a deployment-diagram prompt for this app
#   → a text file with an image prompt (paste into Google "Nano Banana") plus a
#     mermaid block (render in VS Code / GitHub / mermaid.live). No secrets in it.
wbsp deploy --config path/to/wbsp.yaml --destination dev --diagram my-app.diagram.txt

# List local-machine apps. `local` is the umbrella (like bare `aws`): it shows
# EVERY local deployment — compose + dev + standalone — in one listing, each row
# labelled with its type. Name a single type instead to narrow to just that one.
wbsp list --provider local                    # all local types at once
wbsp list --provider local --tenant <tenant>  # scope to one tenant
wbsp list --provider local --json             # machine-readable (type per entry)
wbsp list --provider compose                  # just one type (compose / dev / standalone)
wbsp list --provider compose --health         # also probe each app

# Show one app's status (auto-detects destination if omitted)
wbsp status <app-name>
wbsp status <app-name> --json

# Stop an app but KEEP its data (database + Redis preserved)
#   The app stops running; deploy it again with the same name to reconnect to
#   the same database. Use this to pause without losing data.
wbsp stop <app-name> --tenant <tenant>
wbsp deploy --config path/to/wbsp.yaml --destination dev   # restart → same DB

# Remove an app and all its resources (DELETES the database — data is lost)
wbsp remove <app-name> --tenant <tenant>
wbsp remove <app-name> --tenant <tenant> --force   # no prompt

Dev — run the app from your IDE against containerised data services

With a type: dev destination, the deploy brings up DB/Redis in containers and writes a .env next to the app (discrete DATABASE_* / REDIS_* on localhost, derived ports). There is no app container and no Traefik — you run the app yourself from your IDE.

The platform derives ports for the data services; it cannot derive one for your app, because it does not start it. Pass the derived port yourself, so your project does not collide with every other one on the machine — for project 356, next dev -p 30356 and the API on 80356. Moving to a different port needs no HAP change: the shared test client accepts the standard callback paths on localhost at any port, so it is an AUTH_REDIRECT_URI edit in .env.dev and nothing else. If your front end and API are separate processes they are two origins here but one in the cloud — see Running under dev in wbsp-client-guidelines.md for what that breaks.

# Bring up DB/Redis + write the .env, then run your app from the IDE
wbsp deploy --config path/to/wbsp.yaml --destination dev
source .env && <run your app from the IDE>   # e.g. npm run dev

# Stop the dev environment but KEEP its data
wbsp stop <app-name> --tenant <tenant>

# Tear it down and DELETE its data
wbsp remove <app-name> --tenant <tenant>

Debugging — logs

wbsp logs <app-name>                 # last 100 lines
wbsp logs <app-name> --lines 500
wbsp logs <app-name> --follow        # stream

Debugging — connect to the local platform database

The platform Postgres runs as a docker-compose service (default container wbsp-postgres-1, user wbsp_admin, password wbsp_local_dev, port 5432).

# psql via the host (if you have psql installed)
PGPASSWORD=wbsp_local_dev psql -h localhost -p 5432 -U wbsp_admin -d <db-name>

# or exec into the container
docker exec -it wbsp-postgres-1 psql -U wbsp_admin -d <db-name>

# list databases (one per app with database.enabled)
docker exec -it wbsp-postgres-1 psql -U wbsp_admin -c '\l'

Debugging — inspect the app container directly

docker ps                                  # find the container
docker logs -f <container>
docker exec -it <container> sh

Sample data (shared with aws.demo)

# Dump the app's local DB to SQL seed files (→ sample-data/ next to the config)
wbsp dump-data --config path/to/wbsp.yaml
wbsp dump-data --config path/to/wbsp.yaml --data-only --exclude-table audit_log

Compose specifics — files, URLs, and limits

A compose deploy generates wbsp.docker-compose.yml next to your wbsp.yaml, auto-seeds POSTGRES_PASSWORD / REDIS_PASSWORD into the destination's .env.<destination> on first deploy, and advertises the app at <app>.<projectNo>.wbsp.local (stable across machines). To resolve *.wbsp.local, either point your workstation at a wildcard-resolving DNS provider (localtest.me-style, recommended for repeat use) or add the per-app /etc/hosts line the deploy output prints when resolution fails.

Local-machine limits vs production: HTTP only on host ports (no Traefik/TLS/WAF edge behaviour), no s3: { enabled: true } on the compose provider (the deploy fails closed), and no Kubernetes features (namespaces, NetworkPolicies, service accounts) — apps that depend on those need a real cluster.

An app's own docker-compose services (using, not providing)

If your app uses extra services for local development, run them with the app's own docker-compose.yaml (this is separate from the platform):

docker compose up -d
docker compose ps
docker compose logs -f <service>
docker compose down                 # stop (keeps named volumes)
docker compose down -v              # stop AND delete volumes (data loss)

To run those same services on AWS beside a deployed app, see Parallel services under aws.


aws

Apps deployed to the platform EKS cluster. Image is built locally, pushed through the platform registry gateway to ECR, and rolled out as a Deployment behind Traefik. The default (API) path needs neither AWS_PROFILE nor AWS keyswbsp login (or WBSP_API_TOKEN in CI) authorises both the deploy and the image push (see Credentials). AWS_PROFILE is required only on the operator --backdoor path.

Basics — deploy, list, status, remove

# Deploy / redeploy (builds + pushes image, rolls out) — `prod` is an aws destination
wbsp deploy --config path/to/wbsp.yaml --destination prod

# List apps (queries the cluster across tenants)
wbsp list --provider aws
wbsp list --provider aws --tenant <tenant>
wbsp list --provider aws --json
wbsp list --provider aws --health

# Status of one app (enclave auto-detected; pass --enclave to disambiguate)
wbsp status <app-name> --tenant <tenant>
wbsp status <app-name> --tenant <tenant> --json

# Remove an app and its resources
wbsp remove <app-name> --tenant <tenant>
wbsp remove <app-name> --tenant <tenant> --force

Debugging — logs

wbsp logs <app-name> --tenant <tenant> --lines 200
wbsp logs <app-name> --tenant <tenant> --follow

Connect to an app's platform database — wbsp db (preferred)

Apps with database.enabled get a private RDS database. RDS is not publicly reachable, but wbsp db tunnels to it through a persistent in-cluster relay (no manual relay/credentials needed). It reads the app's connection from the deployed pod env, so the app must be deployed first.

# Run migrations (the headline use case) — opens a tunnel, runs the command, tears down
wbsp db run --config wbsp.yaml -- npx prisma migrate deploy

# Interactive psql against the app's database
wbsp db connect --config wbsp.yaml

# Hold a tunnel open for a GUI client (TablePlus/DBeaver/…) — prints localhost:PORT + creds
wbsp db proxy --config wbsp.yaml      # auto-picks a free local port; --port to override

# Print the connection string
wbsp db url --config wbsp.yaml

What db run injects. The wrapped command gets the SAME discrete variables a deployed pod receives — so a migration or seed script written against them works identically in both places:

VariableNotes
DATABASE_HOST127.0.0.1 on aws (the local end of the tunnel)
DATABASE_PORTthe local tunnel port on aws, the real port locally
DATABASE_NAME
DATABASE_USER
DATABASE_PASSWORDnever printed
DATABASE_SSLtrue only when the connection uses TLS
DATABASE_URLextra — a convenience for tools that only take a connection string. The platform does NOT inject this into pods; prefer the discrete variables in application code.

Over the aws tunnel the server certificate's CN is the RDS host, never localhost, so CA verification cannot succeed — use require/no-verify. The injected DATABASE_URL already carries sslmode=require&uselibpqcompat=true for exactly this. db run and db connect print the resolved target (destination "aws" → user@127.0.0.1:54321/dbname …) before running, so a connection failure inside a driver still tells you where it was pointed.

  • The relay is provisioned with the platform (wbsp-platform provision --provider aws creates wbsp-db-proxy in the wbsp-system namespace); if it's missing, the commands say so.
  • A local-machine destination (compose/dev/standalone) connects straight to the docker-compose database (no relay).
  • The password is shown only by db proxy (so you can paste it into a GUI); other commands never print it.

Technical — parallel services environment

Run an app's docker-compose services (postgres, redis, forgejo, …) in a dedicated, isolated namespace beside the app. Full guide: parallel-services-guide.md.

# From the app directory (with a wbsp-services.yaml environment definition)
wbsp services up   --config wbsp-services.yaml            # additive
wbsp services up   --config wbsp-services.yaml --prune    # reconcile (remove dropped, keep volumes)
wbsp services down --config wbsp-services.yaml            # stop, retain volumes
wbsp services down --config wbsp-services.yaml --purge    # delete namespace + volumes (data loss)

# Redeploy the app afterwards so the connection env (<SERVICE>_HOST/_PORT) is injected
wbsp deploy --config wbsp.yaml --destination prod

Reaching the parallel-services containers directly (kubectl port-forward, Forgejo admin token, repo seeding) requires direct cluster/AWS access and is an operator task, outside the wbsp workflow.


aws.on-demand

Apps deployed as AWS Lambda functions (scale-to-zero, invoked on demand) rather than always-on Deployments — an aws destination with mode: on-demand. Same wbsp verbs as a normal aws destination; routing and ECR are shared, the compute backend differs. Below, lambda names an aws/mode: on-demand destination.

Basics — deploy, list, status, remove

wbsp deploy --config path/to/wbsp.yaml --destination lambda

wbsp list   --provider aws.on-demand
wbsp list   --provider aws.on-demand --json --health

wbsp status <app-name> --tenant <tenant>
wbsp remove <app-name> --tenant <tenant> --force

Debugging — logs

wbsp logs <app-name> --tenant <tenant> --lines 200
wbsp logs <app-name> --tenant <tenant> --follow

Technical — database & parallel services

mode: on-demand apps use the same platform RDS — see connect to an app's database.

They cannot join a parallel-services environment. That environment reaches its allow-listed apps through Kubernetes network rules matched on the app's own namespace, and an on-demand app is a Lambda function with no namespace to match. The parallel services guide states the same limit; if you need those services, deploy to an ordinary aws destination.


aws.demo

Throwaway sample-mode instances: a single image with sample data baked in, run as ephemeral pods in the cluster, each with its own bundled database, on demo subdomains (WBSP_SAMPLE_BASE_DOMAIN). They are not Lambda functions and have no platform RDS.

demo is a reserved destination name, like universal below: you declare demo: {} and the platform supplies its whole identity — no type, mode, tenant, cluster or enclave (declaring any of them is refused).

Two steps, and it matters that they are separate: prepare builds and pushes the sample image, deploy --destination demo publishes it — and starts nothing. An instance begins when somebody launches one, with wbsp demo launch or from the website. Nobody visits a URL because you deployed.

One-time — prepare the sample image

# (If the app has a database) generate seed SQL from your local DB first
wbsp dump-data --config path/to/wbsp.yaml

# Build + push the sample image (sample data baked in)
wbsp prepare --config path/to/wbsp.yaml
wbsp prepare --config path/to/wbsp.yaml --destination demo   # the demo destination

Basics — publish, then launch

# Publish the sample image for the demo destination. Starts nothing.
wbsp deploy --config path/to/wbsp.yaml --destination demo

# Start an ephemeral instance of it. This is what produces a live URL.
wbsp demo launch   --variant <variant-id>
wbsp demo status   --variant <variant-id>   # availability + live instances
wbsp demo remove   --instance <instance-id> # end one now, without waiting for its TTL

wbsp list   --provider aws.demo
wbsp list   --provider aws.demo --json --health

Instances expire on their own: wbsp demo reap advances the expired ones from their time limit, through a grace page, to removal. Run wbsp demo <command> --help for each command's exact flags.

Debugging — logs

wbsp logs <app-name> --tenant <tenant> --lines 200
wbsp logs <app-name> --tenant <tenant> --follow

Demo apps carry their data inside the image (no platform RDS), so there is no external database to connect to. Wildcard TLS for the demo subdomains is provisioned and managed by the platform (an operator concern).


universal

Publishes an appliance-runnable image and stops. Nothing is deployed, nothing runs, no database or route is provisioned, and no tenant is involved at any point. This is the destination to use when you want your app to run on a home appliance (wbsp-vm) or any Apple Silicon machine, and it is the whole flow for an appliance-only app that declares no cloud destination at all.

universal is a reserved destination name: you declare the block, and the platform supplies its entire identity — no type, tenant, cluster or enclave (declaring any of them is a hard error).

destination:
  universal: {}

Basics — publish

# Build for linux/amd64 + linux/arm64 and push as one multi-architecture index
wbsp deploy --config path/to/wbsp.yaml --destination universal

# --destination may be omitted when `universal` is the only destination declared
wbsp deploy --config path/to/wbsp.yaml

It reads .env.universal (same one-file-per-destination rule as every other placement) and prints a machine-readable result:

{
  "app": "my-app",
  "destination": "universal",
  "image_ref": "registry.wbsp.ai/<owner>/<variant>/app:latest",
  "platforms": ["linux/amd64", "linux/arm64"],
  "status": "published",
  "deployed": false
}

"deployed": false is the point of the destination, not a footnote.

There are no lifecycle commands

list, status, logs, stop and remove do not apply — nothing was deployed, so there is nothing to inspect or tear down. To verify a publish, read the image_ref and platforms above.

Credentials — login only, never --backdoor

Publishing goes through the registry gateway with your wbsp login session and needs no AWS credentials:

wbsp login                       # required
export WBSP_API_URL=<your wbsp-api endpoint>   # required

--backdoor is refused for this destination — the error names the reason. See credential-less-deploy.md.

Deploying to the cloud as well — order matters

A cloud deploy publishes the cloud architecture only, moving the image tag off any appliance index published earlier. Run the universal publish last:

wbsp deploy --config path/to/wbsp.yaml --destination aws         # cloud
wbsp deploy --config path/to/wbsp.yaml --destination universal   # restore appliance runnability

A cloud deploy of an app that declares universal: prints a reminder to that effect.

Declare release: where the appliance will see it

An appliance cannot read your wbsp.yaml — it reads the run manifest stamped on the image, built from this destination's effective configuration. An empty universal: {} inherits your top-level release:; override it under the block when the appliance needs something different:

destination:
  universal:
    release: "node scripts/migrate-appliance.js"
    env:
      TELEMETRY: "off"

Troubleshooting

MessageMeaning
no destination "universal" is declaredAdd universal: {} under destination: — declaring the block is how an app opts into appliance publishing.
--backdoor is not supported for the "universal" destinationDrop --backdoor; publish with your wbsp login session.
WBSP_API_URL is required to publish to the "universal" destinationSet WBSP_API_URL to your wbsp-api endpoint.
declares neither source: nor dockerfile: — there is nothing to builduniversal publishes an image; give it something to build.
Multi-platform build is not supported for the docker driverThe wbsp-multiarch buildx builder could not be provisioned. Check docker buildx version, or set WBSP_BUILD_PLATFORMS=linux/amd64 to publish a cloud-only image (which will not run on an appliance).
--publish-only is retired / --universal is retiredBoth are replaced by --destination universal.

First publish may create a wbsp-multiarch builder. A stock Docker Desktop cannot build multi-platform images, so the CLI provisions a dedicated buildx builder on first use and reuses it afterwards. Do not delete it between publishes. WBSP_BUILD_PLATFORMS (comma-separated) narrows the published set for a pipeline that has measured the cost.


Rebuilding the binaries

After any Go change, rebuild all three binaries into bin/ with the version stamped in (--version reads it):

make build          # builds wbsp, wbsp-platform, wbsp-api into bin/ with -ldflags version

# Quick gate
go build ./... && go test ./...

Installing them onto another machine (clients/operators) is covered in installation.md.

Help for any command

wbsp --help
wbsp <command> --help          # e.g. wbsp deploy --help
wbsp services --help
wbsp-platform --help
wbsp-platform <command> --help

External reference documentation

The underlying tools the platform builds on — consult these when a command's behaviour or flags go beyond what's covered above.

Traefik (ingress)

Docker & Docker Compose

Forgejo (the worked-example parallel service)

PostgreSQL

Prisma (used by the example app)