HOWTO: Bootstrap a new WBSP platform

When you need this

Run this once when you stand up a new WBSP platform — a fresh EKS cluster with wbsp-api (the platform control API served at api.wbsp.ai). It covers the handful of settings that make sign-in work and the one grant that lets the first person deploy.

There is no "first admin" to create. Since feature 093 the platform holds no list of who may act where: who can deploy is decided in the identity provider, tenant by tenant, and the platform reads it from each credential. Nothing here writes to the platform's database by hand, and nothing needs a restart to take effect.

What this actually does (plain English)

wbsp-api is the platform's control plane. It accepts one kind of bearer credential: one issued by the identity provider (HAP) for a tenant — a person's, after wbsp login --tenant <tenant>, or a service account's, from the OAuth client-credentials grant. The credential's issuer names the tenant (https://<hap-host>/t/<tenant>); a permissions claim (people) or scope claim (service accounts) names what its holder may do there. The platform signs nothing itself and stores nobody's access.

Three non-obvious truths dictate the steps below:

  • The tenant is the issuer. Only a tenant's own keys verify a credential minted for it, so a credential from one tenant is refused by every other before any claim is read. That refusal is the tenant boundary — there is no list to keep in sync.
  • HAP stamps each token's aud with the client it was issued to. The built-in CLI client (hap_cli, present in every tenant) and the config UI's own client are accepted by default; WBSP_API_AUDIENCE only adds one more.
  • Authority travels in the token. A role granted after a token was minted shows up at the next refresh (HAP re-evaluates permissions on refresh) or the next sign-in — never by restarting anything.

Prerequisites

  • HAP is already deployed and the platform's own tenant exists in it (e.g. https://hap.wbsp.ai/t/wbsp). You hold its admin key (hapk_…) and the tenant's UUID for the optional steps. (HAP is its own project/bootstrap — out of scope here.)
  • AWS credentials (AWS_PROFILE=…) with access to the cluster and RDS.
  • Docker running locally (the deploy builds the image on your machine).
  • The wbsp and wbsp-platform binaries built to bin/ (make build).
  • The wbsp-api deploy spec cmd/wbsp-api/wbsp.yaml and its secret file cmd/wbsp-api/.env.wbsp (git-ignored).

Throughout, replace <…> placeholders. Concrete example values from the reference platform are shown in comments.

Steps

1. Point wbsp-api at HAP

What: set the ONE identity value wbsp-api needs: the provider's base URL. Why (feature 093): every tenant's issuer derives from the base plus the tenant name, the verification keys derive from the issuer, and the authorisation scope is the issuer — so a new tenant needs no configuration change and no restart.

Edit cmd/wbsp-api/wbsp.yaml under env::

env:
  WBSP_HAP_BASE_URL: https://<hap-host>                 # e.g. https://hap.wbsp.ai — NO /t/<tenant>
  WBSP_HAP_ISSUER: https://<hap-host>/t/<tenant>        # the config UI's own sign-in tenant
  WBSP_SAMPLE_BASE_DOMAIN: <demo-wildcard-domain>       # e.g. wbsp-demo.com — only if using demo mode

Two related wirings on the wbsp-api pod (feature 075):

  • WBSP_WEBSITE_URL (env, defaults set in cmd/wbsp-api/wbsp.yaml) — backs wbsp whoami's variant resolution via the website's /api/internal/variant-resolve. Unset ⇒ whoami replies 502 "not configured".
  • Optionally set matching shared secrets WBSP_VARIANT_RESOLVE_SECRET (wbsp-api) and VARIANT_RESOLVE_SHARED_SECRET (website) to gate that endpoint, mirroring the gateway ForwardAuth pattern.

2. Deploy wbsp-api

What: build the image, push to ECR, and roll out the Deployment. Why: --backdoor builds/pushes/applies directly against the cluster using your AWS creds — the path that exists before anyone can sign in, and the recovery path that must keep working when the identity provider cannot be reached.

cd cmd/wbsp-api
AWS_PROFILE=<profile> ../../bin/wbsp deploy --backdoor --destination wbsp

Verify:

curl -sS https://api.wbsp.ai/healthz   # → ok
# the pod log should say: "per-tenant verifier ready (feature 093)"

3. Grant deploy authority — in the tenant, at the provider

What: in HAP's administration for the tenant (its console, or the admin API), grant the person the role wbsp-platform:deploy. Why: that role carries every platform verb for the tenant — deploy, stop, restart, status, logs, remove, maintenance, configuration read and write, enclave management. wbsp-platform:config-read exists as the one lesser grant. There is no wbsp-platform:admin role and no platform-side administrator.

For the platform's own tenant (wbsp) that role can redeploy the platform itself, so grant it rarely; the many people registered in wbsp are there to use its applications and hold no deploy role.

Then sign in to the tenant and read back what the platform established — from the credential alone:

# The CLI defaults to the wbsp.ai platform; export these only when YOUR values differ:
export WBSP_API_URL=<your wbsp-api endpoint>
export WBSP_HAP_BASE_URL=https://<hap-host>
wbsp login --tenant wbsp      # opens a browser; sign in

curl -s -H "Authorization: Bearer $(python3 -c 'import json,os;print(json.load(open(os.path.expanduser("~/.config/wbsp/tokens/wbsp")))["access_token"])')" \
  https://api.wbsp.ai/api/v1/me
# → {"sub":"…","tenant":"wbsp","authority":["wbsp-platform:config-read",…,"wbsp-platform:deploy",…],"kind":"person",…}

An empty authority means the grant was made after the token was minted: sign in again (or wait for the silent refresh).

Bootstrapped. Every further tenant is the same three lines: create it at the provider, grant the role, sign in to it. No platform configuration is edited, nothing is restarted, nobody on the platform side is asked.

Programs that call the platform

A program with no person present — a pipeline, the website's demo launcher — gets its own service account: a confidential application in the tenant it serves, created by that tenant's administrator with the scopes its work needs (none, for a caller whose routes gate on tenant membership only):

curl -sS -X POST https://<hap-host>/v1/applications \
  -H "X-Admin-Key: $HAP_ADMIN_API_KEY" -H "X-Tenant-Id: <tenant-uuid>" \
  -H "Content-Type: application/json" \
  -d '{"name":"<what it is>","application_type":"web","confidential":true,
       "scopes":[],"redirect_uris":[],"access_mode":"open"}'
# → client_id + client_secret (the secret is shown ONCE)

The program mints its own short-lived tokens (3600 s by default) and presents them as WBSP_API_TOKEN or a plain bearer:

POST https://<hap-host>/t/<tenant>/oauth2/token
grant_type=client_credentials&client_id=…&client_secret=…

Revoking it is disabling the application at the provider — effective within one token lifetime. The platform mints no credentials of its own.

Optional: enable the config UI and identity injection (feature 089)

Two once-per-platform steps give the platform its own browser configuration surface (/ui) and let installs mint their own sign-in clients. Both use the HAP admin key from the prerequisites.

  1. The config UI's sign-in client. Register a confidential web client for the UI itself (the same POST /v1/applications as above): name: "wbsp-config-ui", application_type: "web", confidential: true, scopes: ["openid","profile","email"], redirect_uris: ["https://<platform-api-host>/ui/auth/callback"]. Put the minted credentials in wbsp-api's .env.<destination>:

    WBSP_UI_CLIENT_ID=hap_…          # from the response
    WBSP_UI_CLIENT_SECRET=…          # shown once — capture immediately
    # optional: a durable session key (absent, restarts sign everyone out)
    WBSP_UI_SESSION_KEY=$(openssl rand -hex 32)

    The UI verifies its ID tokens with the same per-tenant verifier the API uses, so a browser session acts on the tenant the sign-in established. The UI always sends consent=granted on the authorize redirect, so no consent-loop handling is needed. Without these variables wbsp-api simply does not mount /ui; the JSON API is unaffected.

  2. Identity-provider administration for installs. Give wbsp-api the HAP admin key, in the same .env.<destination>:

    WBSP_HAP_ADMIN_KEY=…             # the HAP admin API key
    # optional: WBSP_HAP_ADMIN_URL if the issuer host is not reachable in-cluster
    # optional: WBSP_HAP_TENANT_ID — only for clients registered before feature 093

    With this set, wbsp deploy --image of an application that declares an authentication callback registers a per-installation sign-in client automatically, in the tenant the installation deploys to — no more hand-run POST /v1/applications per app. Absent, such installs refuse with a sentence naming the variable; identity-less images deploy as always.

Redeploy wbsp-api after adding either block (both ride its declared secrets in cmd/wbsp-api/wbsp.yaml).

Optional: enable on-demand demo mode (feature 072)

If the platform will serve "Try Me" demos, add these once:

  1. Shared demo HAP client — register a confidential client named demo in the demo tenant with a wildcard redirect https://*.<demo-domain>/api/auth/callback/hap (the same POST /v1/applications as above, with confidential:true and scopes including offline_access). Store its {issuer, client_id, client_secret} in a k8s Secret wbsp-demo-hap (namespace wbsp-demo-config) — wbsp-api injects it into every demo pod so a wbsp.ai session carries into the demo via silent SSO. HAP host-wildcard redirect support is required (it ships in headless-auth).
  2. Base domainWBSP_SAMPLE_BASE_DOMAIN in wbsp.yaml (step 1).
  3. A service account for whatever launches demos (e.g. the website), in the demo tenant, with no scopes — see "Programs that call the platform" above. The launcher mints its own tokens; nothing is minted here. Full website integration contract: notes/handoff-wbsp-website-demo.md.

Troubleshooting — symptoms that are bootstrap state, not bugs

SymptomCause / fix
wbsp login opens the browser, you enter the code, then it returns to the login screen with no errorHAP needs consent=granted for first-party clients. Recent wbsp sends it automatically — rebuild/update your binary.
/me shows "authority" empty (or absent) right after the grantThe token predates the grant. Sign in again, or wait for the silent refresh — HAP re-evaluates permissions on refresh.
403 naming a role and an administration pageExactly what it says: you hold no wbsp-platform:deploy in that tenant. The message names where that tenant's administrator grants it.
403 wrong_tenant — "signed in to X, this acts on Y"Your credential is for another tenant. wbsp login --tenant Y (both sessions are kept).
404 unknown_tenantThe tenant does not exist at the provider. An operator creates it there — never from wbsp.
503 provider_unreachableNot a permissions problem: wbsp-api could not reach HAP to verify. Check the pod's egress and WBSP_HAP_BASE_URL.
Login token rejected (401 wrong_audience)The token was minted for a client the platform does not accept. Use the built-in hap_cli client (recent wbsp does) or add the client id via WBSP_API_AUDIENCE.
invalid_scope … exceeds application scopes during loginThe client you are signing in with cannot ask for a refresh token: either a hand-made CLI client registered without offline_access (use the built-in hap_cli client instead), or a HAP older than migration 0018 (2026-09-02), which seeded the built-in client without that scope. The remedy is at the provider, not in the CLI.
Demo launch returns 412WBSP_SAMPLE_BASE_DOMAIN not set on wbsp-api.
Demo launch returns 404 … not publishedThe app hasn't been published for that tenant (wbsp deploy --destination <demo>), so there's no launch config to start from.