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 published image sits in ECR (at-rest cost only).

Stage 5 — Debugging and audit (see the output, know who tried it)

Who: the app creator whose sandbox misbehaves; an operator. Tool: wbsp demo logs / wbsp demo audit, or the same endpoints from the website. No cloud or cluster credentials — the platform reads the pod with its own.

An instance's log output is readable at every stage: while it warms, while it runs, after it failed to start, and — from a snapshot the platform takes just before the pod is destroyed — for 7 days after it is torn down. Lines are timestamped and tagged app (the application's own output) or platform (the instance's start-up steps: Postgres init, seed load, app start).

wbsp demo logs --sandbox                  # in the app dir: your single live instance
wbsp demo logs --sandbox --name <id>      # a named one — also after it was reaped
wbsp demo logs --sandbox --follow         # stream live output (Ctrl-C to stop)
wbsp demo logs --sandbox --lines 500 --since 30m

Who may read: the person who launched the instance (or was launched for), anyone holding logs authority in tenant demo (operators), and the variant's owners and collaborators as the catalog knows them. A visitor of a public demo never can.

The trial audit log records every launch attempt made through the platform — accepted and refused (at capacity, not published, not permitted): who it was for, the credential that made the call, the application, when it launched and became ready, when and why it ended (expired, ended early by the launcher, removed by an operator, failed to start, crashed, lost), and two durations — held (launch → end) and in use (ready → end). Records outlive the instance and are kept for at least a year.

wbsp demo audit --sandbox                 # this variant, last 30 days
wbsp demo audit --sandbox --who phil --outcome closed --since 7d
wbsp demo audit --all --since 90d --csv > trials.csv    # every variant (operators)

For a website "Try Me" launch the record names the visitor once the website sends the visitor's identity with the launch (an optional visitor: {id, label} body field the platform honours only from the website's service credential); until then it reads "unidentified visitor via the website" rather than nothing. A person launching with their own credential is always recorded as themselves.

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; output snapshotted first
Debugapp creator / operatorwbsp demo logsGET …/instances/{id}/logslive pod output, or the 7-day snapshot after teardown
Auditoperator (website later)wbsp demo auditGET …/demo/audit, GET /tenants/{t}/demo-auditevery trial: who, when, how long, how it ended

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}

    CLI->>API: GET …/instances/{id}/logs (--follow)
    API->>Pod: read pod output (platform credentials)
    API-->>CLI: timestamped lines

    Note over Reaper: at expires_at
    Reaper->>Pod: snapshot output, then delete app pod → serve "demo ended" (grace)
    Note over Reaper: after grace
    Reaper->>Pod: delete namespace + route (gone, scale-to-zero)
    Note over API: trial record closed: expired, held/in-use durations

    CLI->>API: GET …/demo/audit
    API-->>CLI: who / when / how long / how it ended

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.
  • protocols/protocol-11-launch-instance.md — the launch, logs and audit endpoints in wire detail.