One-command, phone-reachable CHAPI demo environment.
docker compose up starts the three CHAPI demo sites
(chapi-demo-wallet,
chapi-demo-issuer,
chapi-demo-verifier)
in containers, exposes each through a
Cloudflare quick tunnel
(*.trycloudflare.com), and auto-points all of them at a credential
mediator (authn.io)
running on your host — also tunneled, so real phones can complete the
full register → issue/store → present flow against your local mediator
build.
Testing local authn.io changes on a real phone normally requires:
/etc/hosts entries, self-signed-certificate acceptance on every
origin, hand-editing each demo's config.js, and a tunnel that doesn't
break CHAPI. The manual version of this takes an afternoon; this stack
makes it docker compose up.
Quick tunnels are used deliberately instead of ngrok's free tier: ngrok's browser interstitial requires a cookie that iOS refuses to send in cross-site iframe contexts, which silently breaks the hidden CHAPI mediator/wallet iframes on iPhones. Cloudflare quick tunnels serve no interstitial.
- Docker (with Compose v2).
- A credential mediator running on the host at
https://localhost:33443— e.g. an authn.io checkout runningnode authn.localhost.js. The mediator stays on the host (not containerized) so webpack watch / live development keeps working; self-signed certificates are fine (the tunnel skips TLS verification for the host hop).
docker compose up -d && ./urls.shurls.sh blocks until every tunnel is up, then prints the three
phone-ready URLs as the last thing in your terminal. (Plain
docker compose up works too, but the attached logs scroll the URL
block away — re-print it any time with ./urls.sh.) Then on the phone,
in order:
- wallet — tap LOGIN to register the wallet with the mediator.
- issuer — Issue and Store a credential.
- verifier — Present the credential.
Re-print the URLs of a running stack any time:
./urls.shThe stack runs in the background and stays publicly reachable until stopped — don't forget it:
docker compose down(urls.sh prints this reminder too.)
All state is ephemeral (repos are re-cloned and tunnel URLs re-minted on
every up), so down + up is already a fresh start. To also re-pull
the images, exactly like a first run on a new machine:
docker compose down --rmi all --volumes --remove-orphans
docker compose up -d && ./urls.shphone ──► *.trycloudflare.com ──► cloudflared sidecar ──► demo container
│
host mediator (authn.io :33443) ◄── mediator-tunnel (no-tls-verify)
- The
cloudflaredimage is distroless (no shell), so each tunnel runs the stock binary and exposes its assigned quick-tunnel hostname via cloudflared's metrics endpoint (http://<tunnel>:2000/quicktunnel). - Each demo container polls the mediator tunnel's endpoint for that URL,
clones its repo at start, generates
config.jswithMEDIATORpointed at it (theconfig.jsoverride hook exists in every chapi-demo repo for exactly this purpose), and serves over plain HTTP with caching disabled (http-server -c-1). TLS terminates at the Cloudflare edge, so browsers still get a secure context. - Each demo gets its own quick tunnel; the
urlsone-shot service collects and prints all of them.
The demos float on credential-handler-polyfill@3, which currently
resolves to a version that throws on iOS WebKit (every iOS browser) —
see
credential-handler-polyfill#51.
The entrypoint pins 3.0.2 (controlled by POLYFILL_PIN in
docker-compose.yml); drop the pin once #51 is fixed upstream.
- Ephemeral URLs. Quick-tunnel hostnames change on every
compose up; the phone wallet must re-register each session. For stable hostnames, swap quick tunnels for named Cloudflare tunnels (free account + domain required) — the compose layout supports swapping the tunnel entrypoint without touching the demo services. - Best-effort transport. trycloudflare.com has no SLA and may be rate-limited. Fine for development/testing; do not demo to an audience on it.
- Public exposure. While the stack runs, the demos and your local mediator are reachable from the public internet at unguessable URLs. They serve only static demo content and your mediator UI, but shut the stack down when not in use.
- The demos are cloned at container start (
mainbranch). To test local demo checkouts instead, bind-mount one over/appindocker-compose.yml. - The mediator is intentionally not containerized. The primary use
case is testing local authn.io changes, so the mediator runs on the
host where webpack watch keeps your edits live; containerizing it
would freeze a snapshot instead. A fully self-contained mode (an
optional
mediatorservice building authn.io from GitHub, or bind-mounting a local checkout) is a possible future enhancement.