WBSP Platform Architecture

Audience: Anyone who wants to understand how the WBSP platform is structured, how its components interact, and how the same application code runs across different environments.

Related guides: Application Creator Guide · Platform Operator Guide

Table of Contents

  1. Overview
  2. Design Principles
  3. Core Components
  4. Adaptor Architecture
  5. Request Flow
  6. Application Lifecycle
  7. Local-Machine Deployment
  8. AWS Environment
  9. Demo, Sandbox, and Lambda Modes
  10. Google Cloud Platform (GCP)
  11. Microsoft Azure
  12. Vercel
  13. DigitalOcean
  14. Environment Comparison

Overview

The WBSP platform deploys and manages multi-tenant web applications. Application creators provide a Docker image and a YAML config file; the platform handles everything else — ingress routing, database provisioning, container orchestration, and environment variable injection.

The platform exposes three interfaces:

  • wbsp-platform CLI — the operator binary. It provisions and destroys infrastructure, reports platform health, and inventories what is running. It is the only component that reads Terraform outputs.
  • wbsp CLI — the application creator binary. It deploys, inspects and removes applications, manages demos, and handles sign-in. It never reads Terraform outputs; for cloud destinations it calls the REST API, and for local-machine destinations it drives Docker directly.
  • REST API (wbsp-api) — a Go HTTP server (Chi) running as an application on the cluster. It is the control plane: deploy, lifecycle, identity, demo launch, and administration. It is the default path for cloud deploys, and it is what the website calls.

The same application code, the same config file, and the same CLI commands work across all environments. Only the --destination flag changes.

An application config declares a destination: map (placement) plus components (workloads), and the deploy flag is --destination. See wbsp-yaml-reference.md for the full field reference.


Design Principles

Adaptor-based abstraction

Every external dependency — cloud infrastructure, databases, ingress — is accessed through a Go interface. Each target environment provides its own implementation. Adding a new cloud provider means writing three adaptors (cloud, database, ingress) without touching the core deployment logic.

Spawn on demand

Platform services are only started when an application requires them. If no application declares a database, no database is provisioned. This keeps the local development environment lightweight and cloud costs low.

Multi-tenancy with isolation

Applications belong to tenants. Routing, database names, and container names are all scoped by tenant. Applications within a tenant cannot see or affect applications in another tenant.

Same config everywhere

A single wbsp.yaml file describes an application completely. The platform interprets it identically across environments. Developers test locally with a local-machine destination (type: dev / compose / standalone), then deploy to production with an aws destination — the same file, the same discrete DATABASE_*/REDIS_* contract everywhere.


Core Components

┌─────────────────────────────────────────────────────────────┐
│                        wbsp CLI                             │
│                  (cmd/wbsp/main.go)                         │
│          Cobra commands: platform, app                      │
└────────────────────────┬────────────────────────────────────┘
                         │
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
   ┌─────────────┐ ┌──────────┐ ┌────────────┐
   │  Platform   │ │ Deployer │ │   Router   │
   │ Provisioner │ │          │ │            │
   └──────┬──────┘ └────┬─────┘ └─────┬──────┘
          │              │             │
          ▼              ▼             ▼
   ┌────────────────────────────────────────────────────┐
   │                 Adaptor Interfaces                 │
   │  cloud.P  compute.P  db.P  ingress.P               │
   └───┬──────────┬────────┬──────────┬─────────────────┘
       │          │        │          │
   ┌───┴───┐ ┌───┴───┐ ┌──┴───┐ ┌───┴─────┐
   │local/ │ │local/ │ │local/│ │ local/  │
   │ aws/  │ │ aws/  │ │ aws/ │ │  aws/   │
   │ gcp/  │ │ gcp/  │ │ gcp/ │ │  gcp/   │
   └───────┘ └───────┘ └──────┘ └─────────┘

Platform Provisioner (internal/platform/)

Orchestrates the startup and teardown of the entire platform. Calls the cloud provider to create infrastructure, then verifies that the database service and ingress are healthy. Tracks platform state (healthy, degraded, stopped).

Deployer (internal/deploy/)

Manages the application lifecycle: deploy, remove, and transitions between states. Provisions databases, starts containers, configures routes, and writes .env files. For type: dev destinations it brings up only the data-service containers and writes a .env next to the app — the app itself runs from the developer's IDE, so no app container or proxy is started.

Contains three key types:

  • Deployer — the orchestrator that coordinates all operations
  • Registry — in-memory store of all registered applications and their state
  • Lifecycle — state machine defining valid status transitions (pending → deploying → running → removing → removed, plus the dev state)

Router (internal/routing/)

Two-layer routing system:

  • Resolver — in-memory route table that maps (domain, path) → application ID. Detects conflicts before they reach the ingress.
  • Router — coordinates the resolver with the ingress adaptor. When a route is added, the resolver registers it and the ingress provider applies it.

REST API (cmd/wbsp-api/)

A Chi-based HTTP server deployed onto the cluster as an ordinary WBSP application, with its own RDS database holding the users store, the deploy registry, and hosting records. It is the default path for cloud deploys (--backdoor is the bypass), and it also serves identity (/me), administration, and the demo-launch API the website calls. Two background loops run inside the process: the demo TTL reaper and, when capability grants are enabled, the grant reconciler.

Running it with WBSP_API_DEPLOYER=disabled (local mode, e.g. wbsp-vm) starts the same server without the AWS provider stack: identity, auth and admin work against Postgres alone, while the deploy/lifecycle routes answer 503 not_configured.

Metrics & Logging (internal/metrics/, internal/logging/)

Structured JSON logging via zerolog. Metrics hooks for tracking deployments, failures, and usage.


Adaptor Architecture

The platform uses four adaptor interfaces. Each target environment provides an implementation of all four.

Cloud Provider (internal/adaptor/cloud/)

type Provider interface {
    Provision(ctx context.Context, config map[string]any) (*ProvisionResult, error)
    Destroy(ctx context.Context) error
    Status(ctx context.Context) (*ProvisionResult, error)
}

Responsible for creating and tearing down the underlying infrastructure (Docker Compose, EKS cluster, GKE cluster, etc.).

Compute Provider (internal/adaptor/compute/)

type BuildOptions struct {
    Dockerfile string
}

type Provider interface {
    Build(ctx context.Context, name string, sourceDir string, opts BuildOptions) (imageRef string, err error)
    Start(ctx context.Context, opts StartOptions) error
    Stop(ctx context.Context, namespace string, name string) error
    Logs(ctx context.Context, namespace string, name string, follow bool, lines int) (string, error)
    List(ctx context.Context, labelSelector string) ([]AppInfo, error)
    Status(ctx context.Context, namespace string, name string) (*AppInfo, error)
}

Manages container/pod lifecycle. The local implementation shells out to Docker CLI. The AWS implementation creates Kubernetes Deployments, Services, Namespaces, and NetworkPolicies via client-go, and pushes images to ECR. The Build method accepts a BuildOptions struct containing the Dockerfile path; on AWS it creates an ECR repository, builds with --platform linux/amd64, and pushes the image.

Database Provider (internal/adaptor/database/)

type Provider interface {
    Create(ctx context.Context, appName, tenantName string, opts CreateOptions) (*Credentials, error)
    Drop(ctx context.Context, appName, tenantName string) error
    Credentials(ctx context.Context, appName, tenantName string) (*Credentials, error)
    Status(ctx context.Context) (string, error)
}

Creates per-application databases within the platform's database service. Each app gets its own database, user, and credentials. The Create operation is idempotent — calling it for an existing database succeeds without error. On redeploy, passwords are synchronised via ALTER USER to handle credential rotation.

The Credentials struct includes an SSL field. On AWS (RDS), SSL is always required — the deployer injects DATABASE_SSL=true into the application's environment variables so the app can configure its database client accordingly.

Shared Databases

Applications can share another app's database via the shared_with config field. The deployer looks up the referenced app's persisted credentials from the registry state file (~/.wbsp/.wbsp-state-<target>.json) and injects them into the dependent app. The primary app must be deployed first.

Ingress Provider (internal/adaptor/ingress/)

type Provider interface {
    AddRoute(ctx context.Context, route Route) error
    RemoveRoute(ctx context.Context, domain, pathPrefix string) error
    ListRoutes(ctx context.Context) ([]Route, error)
    Status(ctx context.Context) (string, error)
}

Manages routing rules in the ingress layer. Routes map a (domain, path) pair to a backend container.

Adding a New Environment

To add a new target environment (e.g., GCP):

  1. Create internal/adaptor/cloud/gcp/provisioner.go implementing cloud.Provider
  2. Create internal/adaptor/compute/gcp/gke.go implementing compute.Provider
  3. Create internal/adaptor/database/gcp/cloudsql.go implementing database.Provider
  4. Create internal/adaptor/ingress/gcp/traefik_gke.go implementing ingress.Provider
  5. Wire the new adaptors into cmd/wbsp/main.go under a --provider gcp case

No changes to the deployer, router, or any core logic.


Request Flow

Traffic reaches an application through Traefik, the platform's ingress controller, on the AWS environment. (The local-machine types do not use Traefik at all — see Local-Machine Deployment.)

Browser
  │
  ▼
ALB / NLB                     the platform's single public entry point
  │
  ▼
Traefik (EKS, `traefik` ns)   matches Host (and any path prefix) against
  │                           the app's IngressRoute CRD
  ▼
┌─────────────────────┐
│ app Service → Pod   │       forwarded to the container port
└─────────────────────┘       inside the app's own namespace

How routing is applied

The deployer creates a Traefik IngressRoute CRD (plus any Middleware it needs — path prefix stripping, rate limits, gateway ForwardAuth) in the application's namespace. Traefik watches those CRDs cluster-wide and begins routing as soon as they appear. Certificates are issued per host by the configured ACME resolver.

A route is registered in the platform's own resolver first, which detects a (domain, path) conflict before it reaches the ingress — so two applications cannot silently contend for the same address.

Deployments that register no route

A type: dev destination registers no route at all: only the data services run, and you reach the application directly from your IDE. Demo and sandbox instances get an IngressRoute per instance, on a generated host under the demo wildcard domain.


Application Lifecycle

          ┌─────────┐
          │ pending │
          └────┬────┘
               │
       ┌───────┴───────┐
       ▼               ▼
┌────────────┐    ┌─────────┐
│ deploying  │    │   dev   │◄──────────┐
└─────┬──────┘    └────┬────┘           │
      │                │                │
      ▼           ┌────┴───────────┐    │
┌──────────┐      │ Can transition │    │
│ running  │◄─────┤ to deploying   │    │
└────┬─────┘      │ or removing    │    │
     │            └────────────────┘    │
     │                                  │
     ├──── Can transition to dev ───────┘
     │
     ▼
┌──────────┐      ┌──────────┐
│ removing │─────►│ removed  │
└──────────┘      └──────────┘

┌──────────┐
│ failed   │──── Can retry: deploying or dev
└──────────┘
StatusMeaning
pendingRegistered but not yet started
deployingContainer and resources being provisioned
runningApplication is up, serving traffic from a container
devDeployed to a type: dev destination — only data services run in containers; the app runs from the developer's IDE
failedDeployment or startup failed
removingBeing torn down
removedFully cleaned up

Key transitions:

  • running → dev: dev is a deployment type, not a runtime toggle. Deploying an app to a type: dev destination (wbsp deploy --destination <dev>) brings up only the data services (PostgreSQL/Redis) in containers and writes a .env next to the app; the app itself runs from your IDE. There is no app container and no Traefik proxy. Database preserved.
  • dev → running: Deploy the app to a container-backed destination (type: compose, standalone, or a cloud type such as aws) with wbsp deploy. Database preserved.
  • failed → dev or failed → deploying: Retry from failure.

Local-Machine Deployment

There is no local platform. Feature 063 retired the shared local layer entirely: no local Traefik, no shared PostgreSQL container, no local wbsp-api. wbsp-platform --provider local returns an error. Running an application on a laptop is an application-creator activity that needs no operator involvement.

An app creator picks a local-machine destination type in wbsp.yaml and deploys it with wbsp. Each type is self-contained — its own containers, its own host ports, no shared infrastructure to provision or keep running.

TypeWhat runs locallyTypical use
composethe whole app stack in containers (app + per-app PostgreSQL/Redis)production parity
devonly the data services in containers; the app runs from the developer's IDElive-reload development
standalonethe app and its data services inside a single ephemeral containerthrowaway instances; also the shape a demo image takes

Architecture

┌──────────────────────── Docker (host) ─────────────────────────────┐
│                                                                    │
│  type: compose                     type: dev                       │
│  ┌───────────────────────┐         ┌───────────────────────┐       │
│  │ app        :30NNN→app │         │ (no app container)    │       │
│  │ postgres   :54NNN     │         │ postgres   :54NNN     │       │
│  │ redis      :63NNN     │         │ redis      :63NNN     │       │
│  └───────────────────────┘         └───────────┬───────────┘       │
│                                                │ .env written      │
│  type: standalone                              │ beside the app    │
│  ┌───────────────────────┐                     ▼                   │
│  │ one container:        │              your IDE runs the app      │
│  │ app + postgres + redis│                                         │
│  └───────────────────────┘                                         │
└────────────────────────────────────────────────────────────────────┘

No Traefik and no ingress layer. Each type host-publishes its own ports directly; nothing routes by Host header, so there are no /etc/hosts entries to maintain.

Ports

Host-published ports are derived from projectNo (declared at the top level of wbsp.yaml, or parsed from the leading numeric prefix of the app directory's name), so several projects can run side by side: the first two digits of the conventional port followed by the three-digit, zero-padded project number.

ServiceConventionalProject 041Published
Web app300004130041
PostgreSQL543204154041
Redis637904163041

Only the host-published port changes; container-internal ports stay conventional. A derived value above 65535 folds deterministically into the IANA dynamic range [49152, 65535], and a host-port clash is reported by the deploy's pre-flight.

The environment contract is identical to AWS

All three types inject the same discrete DATABASE_* / REDIS_* variables an aws destination does, so application code needs no change between a laptop and the cloud. Local types simply resolve to localhost (or an in-stack service name) on the derived ports — the app never hard-codes that, because it comes from the variables.

Database provisioning

Each app gets its own PostgreSQL container (or, for standalone, its own in-container instance) rather than a database inside a shared server. Data persists across redeploys and across wbsp stop, and is removed only by an explicit wbsp remove.

Discovery and reconciliation

There is no local registry to consult. wbsp list --provider compose (or --target local) reconciles from the live Docker daemon, matching containers by their labels:

wbsp.tenant=<tenant>      wbsp.app=<name>      wbsp.managed-by=wbsp-platform

The platform-managed types apply these automatically. A hand-written docker-compose.yaml must apply the same pair to every service in the stack, or the platform cannot distinguish it from any unrelated Compose project and it will not appear in the list.

Constraints

  • The local providers deploy one top-level component. A multi-component app must use dev or a cloud destination; there is an early-fail guard rather than a partial deploy.
  • No TLS, no autoscaling, no load balancing across instances.
  • One-time host setup: docker network create wbsp-shared, so a compose stack can reach HAP.

For the full workflow see the Application Creator Guide and useful-commands.md.


AWS Environment

Status: Fully implemented. All CLI commands work with an aws destination (type: aws).

Architecture

┌─────────────────────── AWS Region ────────────────────────────────┐
│                                                                    │
│  ┌─── VPC ──────────────────────────────────────────────────────┐  │
│  │                                                              │  │
│  │  ┌─── Public Subnets ─────────────────────────────────────┐  │  │
│  │  │  ALB / NLB (load balancer)                             │  │  │
│  │  │  NAT Gateway                                           │  │  │
│  │  └────────────────────────────────────────────────────────┘  │  │
│  │                                                              │  │
│  │  ┌─── Private Subnets ────────────────────────────────────┐  │  │
│  │  │                                                        │  │  │
│  │  │  ┌── EKS Cluster ──────────────────────────────────┐   │  │  │
│  │  │  │  ┌── traefik namespace ───────────────────────┐ │   │  │  │
│  │  │  │  │  Traefik (Helm chart, ingress controller)  │ │   │  │  │
│  │  │  │  └────────────────────────────────────────────┘ │   │  │  │
│  │  │  │                                                 │   │  │  │
│  │  │  │  ┌── wbsp-<tenant>-<app> namespace ───────────┐ │   │  │  │
│  │  │  │  │  Deployment + Service                      │ │   │  │  │
│  │  │  │  │  NetworkPolicies (default-deny + allow)    │ │   │  │  │
│  │  │  │  │  IngressRoute + Middleware CRDs            │ │   │  │  │
│  │  │  │  └────────────────────────────────────────────┘ │   │  │  │
│  │  │  └─────────────────────────────────────────────────┘   │  │  │
│  │  │                                                        │  │  │
│  │  │  ┌── ECR ──────────────────────────────────────────┐   │  │  │
│  │  │  │  wbsp-<app> repositories                        │   │  │  │
│  │  │  └─────────────────────────────────────────────────┘   │  │  │
│  │  │                                                        │  │  │
│  │  │  ┌── RDS ──────────────────────────────────────────┐   │  │  │
│  │  │  │  PostgreSQL (managed, multi-AZ)                 │   │  │  │
│  │  │  └─────────────────────────────────────────────────┘   │  │  │
│  │  │                                                        │  │  │
│  │  └────────────────────────────────────────────────────────┘  │  │
│  └──────────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────────┘

Adaptor Mapping

Local ComponentAWS EquivalentAdaptor
Docker ComposeEKS (managed Kubernetes)cloud/aws/provisioner.go
docker runK8s Deployment + Servicecompute/aws/k8s.go
Docker image (local)ECR repositorycompute/aws/ecr.go
PostgreSQL containerRDS PostgreSQL (managed)database/aws/rds.go
Traefik (Docker labels)Traefik IngressRoute CRDsingress/aws/traefik_k8s.go

Compute Provider Abstraction

The Deployer uses a compute.Provider interface to abstract container operations. On local, this calls Docker CLI commands directly. On AWS, it creates Kubernetes resources via client-go.

type Provider interface {
    Build(ctx, name, sourceDir string, opts BuildOptions) (imageRef string, err error)
    Start(ctx, opts StartOptions) error
    Stop(ctx, namespace, name string) error
    Logs(ctx, namespace, name string, follow bool, lines int) (string, error)
    List(ctx, labelSelector string) ([]AppInfo, error)
    Status(ctx, namespace, name string) (*AppInfo, error)
}

When source and dockerfile are set in the app config, the deployer calls Build() before Start(). On AWS, this creates an ECR repository, builds the image with --platform linux/amd64, and pushes it. The returned imageRef (the full ECR URI) is used as the container image in the Kubernetes Deployment.

Namespace Isolation Strategy

Each application gets its own Kubernetes namespace (wbsp-<tenant>-<app>). Within each namespace:

  • default-deny NetworkPolicy — blocks all ingress and egress by default
  • allow-required NetworkPolicy — permits only:
    • Ingress from the traefik namespace (so Traefik can route traffic to the app)
    • Egress to DNS (UDP/TCP port 53) for service discovery
    • Egress to RDS (TCP port 5432) for database access

Deleting a namespace cascades all resources within it (Deployment, Service, NetworkPolicies, IngressRoute CRDs).

ECR Image Management

When an app config includes source and dockerfile fields, the platform builds and pushes images automatically. Each app gets an ECR repository named wbsp-<app-name>. The Build() method on the K8s compute provider:

  1. Creates the ECR repository (idempotent)
  2. Logs in to ECR via aws ecr get-login-password
  3. Builds the Docker image with --platform linux/amd64 (required for EKS AMD64 nodes)
  4. Pushes the image to ECR

On app removal, ECR images are cleaned up. On platform destroy, all wbsp-* repositories are force-deleted before Terraform teardown.

Provisioning

wbsp-platform provision --provider aws validates AWS credentials via aws sts get-caller-identity, then runs Terraform to create:

  • VPC with public and private subnets across availability zones
  • EKS cluster with managed node groups and VPC CNI NetworkPolicy support
  • RDS PostgreSQL instance (multi-AZ capable, private subnets, SSL required)
  • Traefik deployed as a Kubernetes ingress controller via Helm
  • ECR registry for container images
  • Load balancer for public internet access

After Terraform completes, the CLI reads outputs automatically via terraform output -json — operators do not need to manually export EKS/RDS/ECR connection details.

Provisioning takes 15–25 minutes.

Database Provisioning on AWS

The RDS provider uses a Kubernetes-based approach to reach the RDS instance in private subnets. It runs SQL statements via a temporary postgres:16-alpine pod (kubectl run --rm -i) that connects to RDS over the VPC's internal network. This avoids exposing the database to the public internet.

The Create() method runs admin statements (CREATE DATABASE, CREATE USER, ALTER USER for password sync) against the admin database, then runs GRANT ALL ON SCHEMA public against the target database — required by PostgreSQL 15+ which revokes default CREATE privileges on the public schema.

Platform Destroy

wbsp-platform destroy --provider aws performs a cascade teardown:

  1. Lists all deployed applications
  2. Warns the operator and prompts for confirmation
  3. Removes each application (deletes namespaces, cleans up ECR images)
  4. Deletes all wbsp-* ECR repositories
  5. Runs terraform destroy to tear down infrastructure

Differences from a local-machine deployment

AspectLocal-machine typesAWS
Container runtimeDocker on the developer's machineKubernetes pods on EKS
Image registrythe local Docker daemonECR (variant-derived repositories)
Isolationone Compose project per appper-app K8s namespace + NetworkPolicies
Databasea per-app PostgreSQL containerAmazon RDS (managed, backed up)
Redisa per-app Redis containerElastiCache (cache) or MemoryDB (durable)
Ingressnone — ports published on the hostTraefik IngressRoute CRDs behind an ALB/NLB
TLSnoneACME / ACM certificates
DNSnone needed (localhost:<derived port>)operator-managed records to the load balancer
Scalingsingle instanceKubernetes HPA / node autoscaling
Secrets.env.<destination> read at deployresolved into a protected Secret object
StateTerraform not involvedS3 backend with DynamoDB locking

Demo, Sandbox, and Lambda Modes

An aws destination's mode: selects how the workload runs. All three modes share the same platform infrastructure — one VPC, one EKS cluster, one Traefik load balancer, one ECR registry.

ModeDestinationComputeDatabaseLifetime
normal (default)type: awsEKS podsshared RDSpersistent
on-demandtype: aws, mode: on-demandLambda, VPC-attachedshared RDSpersistent, scale-to-zero
demothe reserved demo destinationephemeral EKS podbundled inside the instanceminutes (TTL-reaped)

Demo and sandbox instances (mode: demo)

Feature 072 replaced the earlier Lambda + SQLite model. A demo no longer runs on Lambda and no longer uses a "lightweight" database substitute. It runs the application's real, wire-compatible PostgreSQL (and Redis, when declared) bundled inside an ephemeral EKS pod, seeded from a standard dump. The embedded-database provider and the sample_data.engine field were removed.

Demo mode is a three-part model, and the parts are performed by different actors at different times.

1. PUBLISH   wbsp deploy --destination demo   (or --destination sandbox)
             builds a self-contained image, pushes it, writes a launch-config Secret.
             Starts nothing.

2. LAUNCH    the website's "Try the demo" button, or `wbsp demo launch`
             → POST wbsp-api .../launch → one ephemeral pod + route.

3. REAP      wbsp-api's in-process reaper, once a minute.

Note that deploy on a demo destination returns before the deployer is reached — it is a build-and-publish path, not a deployment. Nothing is running until someone launches an instance.

The published image

The publish step produces a single self-contained image: the shared standalone base (PostgreSQL + Redis + the entrypoint) overlaid with the application image and a seed file concatenated from sample_data.path. Because it is self-contained, an instance needs no RDS, no ElastiCache, and no shared state — which is what makes it cheap enough to launch per visitor and safe to destroy.

The sandbox destination publishes a parallel image and config secret (sandbox rather than demo), so a developer's work-in-progress never disturbs the promoted public demo. Promotion retags the sandbox manifest to the durable :demo tag — no rebuild.

demo and sandbox are two reserved destination names sharing one placement identity (tenant=demo, cluster=main, enclave=demo); the destination name is what selects the published kind. Feature 085 replaced the old --destination demo --sandbox form, so the kind can no longer disagree with the destination the operator named. A sandbox publish with no sandbox: block borrows the demo: block and .env.demo, which is why the change required no config migration.

An instance

┌─── EKS ─────────────────────────────────────────────────────────┐
│  Traefik ──IngressRoute── https://<app>-<token>.<base-domain>    │
│                                  │                               │
│  ┌── per-instance namespace ─────▼──────────────────────────┐    │
│  │  one pod:  PostgreSQL + Redis + the application          │    │
│  │  env = the launch-config Secret                          │    │
│  │      + the shared demo OIDC client (overlaid LAST)       │    │
│  └──────────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────────┘
  • The URL carries an unguessable token, preventing enumeration while letting the app run at the root path /. Wildcard TLS for the demo base domain is issued at provisioning.
  • Configuration comes from the Kubernetes Secret wbsp-<kind>-<tenant>-<variant> in namespace wbsp-demo-config, written at publish time.
  • The shared demo OIDC client (Secret wbsp-demo-hap) is overlaid after the app's own environment so it wins. That is what carries a signed-in website visitor into the demo via silent SSO, with no per-app client and no redirect-URI registration.
  • WBSP_SAMPLE_MODE=true is set. The one behaviour an app must change under it: no worker process exists, so enqueuing a job must run its processor inline.

Reaping

The TTL/grace reaper runs inside the wbsp-api process (go demo.RunReaper(…)), once a minute. It calls Go functions directly and never makes an HTTP request, so it passes through no authorization gate and needs no credential.

At the TTL the heavy pod is freed and the URL serves a "this demo has ended" page for a grace window; after that the instance is removed. Grace-phase entries do not occupy a slot against the global concurrency cap, and the reaper self-heals registry entries whose namespace has already disappeared.

A global concurrency cap (default 10, across every demo and sandbox) bounds cost. Platform admins bypass it.


Lambda (mode: on-demand)

The Lambda compute provider (internal/adaptor/compute/lambda/) implements the same compute.Provider interface as the EKS path, so deploy, status, logs and remove work identically.

Interface methodLambda operation
Build()build a container image with the Lambda Web Adapter, push to ECR
Start()create the function + its Function URL
Stop()delete the function, Function URL, and CloudWatch log group
Logs()read /aws/lambda/<function-name>
List()list functions tagged wbsp-*
Status()function state (Active, Pending, Failed)
┌─── VPC ──────────────────────────────────────────────────────────┐
│  ┌── EKS ─────────────────────────────────────────────────────┐  │
│  │  Traefik: IngressRoute Host(`app.example.com`)             │  │
│  │           → backend: the Function URL                      │  │
│  └────────────────────┬───────────────────────────────────────┘  │
│                       ▼                                          │
│  ┌── Lambda (VPC-attached) ───────────────────────────────────┐  │
│  │  container image from ECR + Lambda Web Adapter             │  │
│  │  Function URL (AuthType: NONE)                             │  │
│  │  ──► RDS PostgreSQL (private subnets)                      │  │
│  └────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘

The Web Adapter is appended to the application's Dockerfile at build time:

COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:1.0.0 /lambda-adapter /opt/extensions/lambda-adapter

It translates Lambda invoke events into HTTP requests against the application's port, so application code needs no change. The generated Dockerfile.lambda is built --platform linux/amd64 and pushed to the same ECR registry the EKS path uses.

Routing goes through Traefik, not the Function URL directly. The deployer creates the function, obtains its Function URL, then creates an IngressRoute mapping the host to that URL as a backend. One ingress point serves every application regardless of compute backend, so domain routing, TLS termination and path-prefix middleware behave identically.

VPC attachment is what gives the function RDS access, using the same RDS provider as mode: normal — the same managed PostgreSQL, SSL, per-app databases and credential rotation. The trade-off is a few seconds of extra cold start.


Legacy: the Lambda demo path

Before feature 072, mode: demo created a non-VPC Lambda with an embedded database in /tmp, cleaned up by a one-shot EventBridge Scheduler rule invoking a wbsp-demo-cleanup function. That path no longer runs: deploy on a demo destination publishes an image instead, and instances are EKS pods.

The plumbing is retained only so the platform can still see and clean up functions created by the old model — wbsp list --provider aws.demo, wbsp-platform inventory --provider aws, and the orphan scan in internal/adaptor/compute/lambda/orphans.go all still enumerate Lambda functions tagged aws.demo / aws.on-demand. Treat any that appear as leftovers to remove, not as current deployments.


Comparing the modes

Aspectnormal (EKS)on-demand (Lambda)demo (ephemeral EKS)
ComputeKubernetes podsLambda, VPC-attachedone Kubernetes pod per instance
DatabaseRDS PostgreSQLRDS PostgreSQLreal PostgreSQL bundled in the instance
RedisElastiCache / MemoryDBElastiCache / MemoryDBbundled in the instance when declared
Seedednonoyes, from a standard dump
Cold startalways warm~5–10s (VPC)~15–60s to first response
Max request durationunlimited15 minutesunlimited
Data persistencepersistentpersistent (RDS)discarded with the instance
Auto-cleanupnonoyes (in-process reaper)
IngressTraefik IngressRouteTraefik → Function URLTraefik IngressRoute
Started bywbsp deploywbsp deploya launch request, after a publish

Google Cloud Platform (GCP)

Status: Planned. No adaptors implemented yet.

Planned Architecture

ComponentGCP Service
Container orchestrationGoogle Kubernetes Engine (GKE)
DatabaseCloud SQL for PostgreSQL
IngressTraefik on GKE or Cloud Load Balancing
Infrastructure as CodeTerraform

Adaptors Required

  • internal/adaptor/cloud/gcp/provisioner.go — GKE cluster via Terraform
  • internal/adaptor/database/gcp/cloudsql.go — Cloud SQL database and user management
  • internal/adaptor/ingress/gcp/traefik_gke.go — Traefik on GKE or GCP Ingress resource

Key Considerations

  • Cloud SQL supports IAM-based authentication as an alternative to password-based credentials
  • GKE Autopilot mode would reduce node management overhead
  • Workload Identity federation for pod-to-service authentication

Microsoft Azure

Status: Planned. No adaptors implemented yet.

Planned Architecture

ComponentAzure Service
Container orchestrationAzure Kubernetes Service (AKS)
DatabaseAzure Database for PostgreSQL (Flexible Server)
IngressTraefik on AKS or Azure Application Gateway
Infrastructure as CodeTerraform

Adaptors Required

  • internal/adaptor/cloud/azure/provisioner.go — AKS cluster via Terraform
  • internal/adaptor/database/azure/pgflex.go — Azure PostgreSQL Flexible Server management
  • internal/adaptor/ingress/azure/traefik_aks.go — Traefik on AKS or Application Gateway Ingress Controller

Key Considerations

  • Azure AD integration for managed identity authentication
  • Azure Database for PostgreSQL Flexible Server supports in-VNET deployment for private access
  • AKS offers a built-in ingress controller (Application Routing) as an alternative to Traefik

Vercel

Status: Planned. Requires a different adaptor pattern — Vercel is serverless, not container-based.

Planned Architecture

ComponentVercel Equivalent
Container orchestrationVercel Deployments (serverless functions + static)
DatabaseVercel Postgres (Neon) or external provider
IngressVercel Edge Network (automatic)
Infrastructure as CodeVercel CLI / API

Adaptors Required

  • internal/adaptor/cloud/vercel/provisioner.go — project creation and deployment via Vercel API
  • internal/adaptor/database/vercel/neon.go — Vercel Postgres (Neon) database provisioning
  • internal/adaptor/ingress/vercel/edge.go — domain and routing configuration via Vercel API

Key Considerations

  • Vercel is serverless — there are no long-running containers. Applications must be compatible with serverless execution (Next.js, SvelteKit, Remix, etc.) or use Vercel Functions.
  • Docker images are not deployed to Vercel directly. The adaptor would need to either deploy from source or use a different packaging model.
  • Vercel handles TLS, CDN, and edge routing automatically — the ingress adaptor would be simpler than Kubernetes-based environments.
  • The database adaptor could target Vercel's built-in Postgres (powered by Neon) or connect to an external database.
  • Local-machine development (type: dev) is less relevant on Vercel since the platform is already optimised for fast iteration with vercel dev.

DigitalOcean

Status: Planned. No adaptors implemented yet.

Planned Architecture

ComponentDigitalOcean Service
Container orchestrationDigitalOcean Kubernetes (DOKS)
DatabaseManaged PostgreSQL
IngressTraefik on DOKS or DigitalOcean Load Balancer
Infrastructure as CodeTerraform

Adaptors Required

  • internal/adaptor/cloud/digitalocean/provisioner.go — DOKS cluster via Terraform
  • internal/adaptor/database/digitalocean/managed_pg.go — Managed PostgreSQL database and user management
  • internal/adaptor/ingress/digitalocean/traefik_doks.go — Traefik on DOKS

Key Considerations

  • Simpler and lower cost than AWS/GCP/Azure for smaller deployments
  • DigitalOcean's managed Kubernetes is straightforward with less configuration surface
  • Managed PostgreSQL includes automated backups and failover
  • App Platform could be an alternative to DOKS for simpler workloads (similar considerations to Vercel)

Environment Comparison

The three AWS modes share one platform; the local-machine types share nothing. "Demo" below is the mode: demo instance, not a separate environment.

CapabilityLocal-machineAWS (normal)AWS (on-demand)AWS (demo)GCPAzureVercelDigitalOcean
StatusCompleteCompleteCompleteCompletePlannedPlannedPlannedPlanned
Provisioningnone — self-containedTerraform → EKSshared AWS platformshared AWS platformTerraform → GKETerraform → AKSVercel APITerraform → DOKS
Container runtimeDockerKubernetes (EKS)Lambda (VPC-attached)Kubernetes (EKS), one pod per instanceKubernetes (GKE)Kubernetes (AKS)ServerlessKubernetes (DOKS)
Databaseper-app Postgres containerRDS PostgreSQLRDS PostgreSQLreal Postgres bundled in the instanceCloud SQLAzure PostgresVercel PostgresManaged Postgres
Ingressnone (host ports)Traefik (K8s)Traefik → Function URLTraefik (K8s)Traefik (K8s)Traefik (K8s)Edge NetworkTraefik (K8s)
TLSNoYes (ACME)Yes (ACME)Yes (wildcard)Yes (managed)Yes (managed)Yes (automatic)Yes (Let's Encrypt)
AutoscalingNoYes (HPA)Yes (automatic)No — one pod, then reapedYes (HPA)Yes (HPA)Yes (automatic)Yes (HPA)
CostFree$$$$ (scale-to-zero)$ (minutes per instance)$$$$$$$$$$
Setup time~1 minute15–25 min<90s (deploy)<90s (publish); ~15–60s (launch)15–25 min15–25 min~2 min10–15 min
Docker imageslocal daemonECRECR + Web AdapterECR (self-contained image)GCR / Artifact RegistryACRN/A (source)DOCR / registry
Auto-cleanupNoNoNoYes (in-process reaper)NoNoNoNo
Data persistencePersistentPersistentPersistent (RDS)Discarded with the instancePersistentPersistentPersistentPersistent
Started bywbsp deploywbsp deploywbsp deploya launch request, after a publishwbsp deploywbsp deploywbsp deploywbsp deploy

The local-machine types (compose, dev, standalone) are developer-machine features: they need no platform, no operator, and no credentials, and they deliver the same DATABASE_* / REDIS_* environment contract as the cloud so application code is unchanged between them.