Demo applications: the full round trip

This explains the complete life of a WBSP demo (aws.demo) — the "Try Me" experience: a real app running on real, wire-compatible datastores, launched on demand at a unique URL, and torn down automatically. It covers every stage (publish → launch → status → reap), who does what (app creator/operator, the wbsp.ai website, an end user, the platform), and which tool drives each step (wbsp CLI and/or the website).

The mental model

A demo is not a persistent deployment. It's an ephemeral, per-visitor copy of an app that:

  • runs the app's real image plus bundled PostgreSQL + Redis inside one pod (no shared RDS/ElastiCache), seeded from the app's standard SQL dump;
  • lives at its own unguessable URL (<app>-<token>.wbsp-demo.com);
  • is isolated (its own namespace, gVisor sandbox where available);
  • auto-expires (default 10 minutes) and is reaped to nothing — zero compute when idle (scale-to-zero);
  • signs the visitor in via shared SSO — if they're logged into wbsp.ai, they're logged into the demo, no second login.

It runs in three parts, done by three different actors, and there is exactly one identifier that ties them together: the catalog variant id (from the app's wbsp.yaml variant:). The CLI reads it from the file, the website holds it as a route param, and wbsp-api uses it as the lookup key — nobody translates between app names, repo names, and ids.

  PUBLISH (once per release)        LAUNCH (on demand)            REAP (automatic)
  app creator / operator            website button / CLI          the platform
  ───────────────────────           ──────────────────           ────────────────
  build image + bake seed     →     start an ephemeral pod   →    TTL → grace page → gone
  push to ECR                       inject datastores + SSO       namespace deleted
  write per-variant config          return a unique URL           scale-to-zero
  (nothing running yet)             (warming → running)

Stage 1 — Publish (build the demo image)

Who: the app creator or a platform operator. Tool: wbsp CLI. When: once per app release. Result: nothing runs; an image + config sit ready in the platform.

Prerequisites in the app's wbsp.yaml:

  • a variant: id (mandatory — this is the demo's key everywhere),
  • a sample_data: block pointing at standard .sql seed files,
  • a demo destination — just demo: {}. The name is reserved: the platform supplies its whole identity (type, mode, tenant, cluster, enclave), and declaring any of those keys is an error.
wbsp deploy --destination demo

What the platform does:

  1. Builds a self-contained image — the app layered on the bundled Postgres+Redis+supervisor stack, with the sample data baked into the seed path — for linux/amd64, and pushes it to ECR (wbsp-v-<short>-app-demo:demo).
  2. Writes a per-variant config Secret (wbsp-demo-<tenant>-<variant>) holding the image ref, the app's env: + resolved secrets:, and markers (WBSP_DEMO_APP_NAME for the display URL, WBSP_DEMO_REDIS to gate Redis). The launch API — which never sees the app's wbsp.yaml/.env — reads this at launch time.

No instance is started. The app is now launchable.

Stage 2 — Launch (start a personal instance)

Who: an end user clicking "Try Me" on wbsp.ai (primary), or an operator via the CLI (parity). Tool: the website → wbsp-api launch API; or wbsp demo launch. Result: a warming, then running, per-user instance at a unique URL.

Via the website (the real path)

  1. A signed-in wbsp.ai visitor clicks Try Me on a variant.
  2. The website's server calls the platform, passing the variant id it already holds and its service token: POST https://api.wbsp.ai/api/v1/apps/{tenant}/{variant}/demo/launch.
  3. The platform (wbsp-api), server-side:
    • reads the per-variant config Secret (image + app env),
    • injects the discrete datastore contract (DATABASE_* / REDIS_* pointing at the in-pod engines), WBSP_SAMPLE_MODE=true, the container port, and the per-instance URL,
    • overlays the shared demo HAP (SSO) config so the visitor's wbsp.ai session carries in,
    • creates the ephemeral k8s objects: a per-instance Namespace, a single Pod (gVisor, TTL backstop), a Service, and a Traefik IngressRoute on <app>-<token>.wbsp-demo.com (wildcard TLS),
    • returns 202 { id, url, status:"warming", expires_at, seconds_remaining }.
  4. The website redirects the visitor to url. First response takes ~30–60s while the pod boots Postgres+Redis, loads the seed, and starts the app. Then the app serves, already signed in, with seeded data.

The website never handles image refs, HAP client ids, or secrets — the platform injects all of it. Concurrency is capped platform-wide (a 409 means "at capacity").

Via the CLI (operator/CI parity)

Run in the app directory — the variant comes from wbsp.yaml, the tenant from the demo destination:

wbsp demo launch
#   url:      https://ai-native-crm-ab12cd34.wbsp-demo.com
#   status:   warming
#   expires:  2026-07-15T10:40:00Z

Config-less callers pass --tenant <t> --variant <id>. (--backdoor is a separate operator-only path that builds against EKS directly with an explicit --image; the website always uses the API.)

Stage 3 — Status (is it available? how long left?)

Who: the website (to decide whether to show the button, and to drive a countdown), or an operator. Tool: the launch API / wbsp demo status.

  • App-level availability — call this before offering a demo: GET /apps/{tenant}/{variant}/demo{ published, prerequisites_ok, launchable, capacity:{active,max,at_capacity}, instances:[…] }. Gate the button on launchable.
  • Per-instance — for the countdown after launch: GET /apps/{tenant}/{variant}/demo/instances/{id}{ status, expires_at, seconds_remaining }.
wbsp demo status          # in the app dir
# variant:     7a1d8030-… (ai-native-crm)
# launchable:  true
# capacity:    1/10 active
# instances:   1 running
#   - ai-native-crm-ab12cd34  [running]  https://…  reaps in 8m41s

seconds_remaining is computed server-side; the website renders "N min left" from it (or from expires_at).

Stage 4 — Reap (automatic shutdown)

Who: the platform, unattended. Tool: the wbsp-api reaper (a background loop) plus a pod-level backstop. No one calls anything — this just happens.

Each instance has an expires_at (launch time + timeout_minutes, default 10). The reaper ticks periodically and moves every instance through a two-phase teardown:

  1. TTL reached → the app Pod (and its in-pod Postgres/Redis) is deleted, and for a short grace window the same URL serves a "this demo has ended" page instead of a connection error.
  2. Grace elapsed → the whole Namespace, the route, and the registry entry are deleted. The URL stops resolving. All demo data is gone.

Defense in depth: the Pod also carries activeDeadlineSeconds, so it self-terminates at the TTL even if the reaper is down. A visitor can also end it early:

wbsp demo remove --name <instance-id>          # or the website's DELETE

When no instances are live, nothing runs — only the prepared image sits in ECR (at-rest cost only).

Who does what — summary

StageActorCLIWebsitePlatform (wbsp-api)
Publishapp creator / operatorwbsp deploy --destination demo (build+push+config)stores image + per-variant config
Launchend user (or operator)wbsp demo launch (parity)"Try Me" → POST …/demo/launchstarts pod, injects datastores + SSO, returns URL
Statuswebsite / operatorwbsp demo statusGET …/demo (+ instance poll)reports availability + countdown
Reapplatformwbsp demo remove (early)DELETE …/instances/{id} (early)TTL → grace page → deleted; scale-to-zero

Auth in one line

Demo apps use a shared demo OIDC client in the wbsp tenant (same tenant as wbsp.ai), injected by the platform at launch. A visitor already signed into wbsp.ai is signed into the demo via silent SSO; a signed-out visitor sees the tenant login page (still works, just not silent).

Sequence (end to end)

sequenceDiagram
    participant Creator as App creator/operator
    participant CLI as wbsp
    participant Site as wbsp.ai website
    participant API as wbsp-api
    participant Pod as Demo pod
    participant Reaper as Platform reaper

    Creator->>CLI: deploy --destination demo
    CLI->>API: build+push image, write per-variant config
    Note over API: nothing running yet

    Site->>API: POST /apps/{tenant}/{variant}/demo/launch
    API->>Pod: create ns + pod (PG+Redis+app) + route
    API-->>Site: 202 {url, status:warming, expires_at}
    Site-->>Pod: redirect visitor to url (silent SSO)
    Note over Pod: warming ~30-60s → running (seeded)

    Site->>API: GET /apps/{tenant}/{variant}/demo/instances/{id}
    API-->>Site: {status:running, seconds_remaining}

    Note over Reaper: at expires_at
    Reaper->>Pod: delete app pod → serve "demo ended" (grace)
    Note over Reaper: after grace
    Reaper->>Pod: delete namespace + route (gone, scale-to-zero)

See also

  • user-docs/HOWTO-bootstrap-developer.md — set up wbsp to run these commands.
  • notes/handoff-wbsp-website-demo.md — the website integration contract (endpoints, auth, gating).
  • user-docs/application-creator-guide.md — the sample_data: + demo-destination config.