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
- Overview
- Design Principles
- Core Components
- Adaptor Architecture
- Request Flow
- Application Lifecycle
- Local-Machine Deployment
- AWS Environment
- Demo, Sandbox, and Lambda Modes
- Google Cloud Platform (GCP)
- Microsoft Azure
- Vercel
- DigitalOcean
- 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-platformCLI — 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.wbspCLI — 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) pluscomponents(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
devstate)
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):
- Create
internal/adaptor/cloud/gcp/provisioner.goimplementingcloud.Provider - Create
internal/adaptor/compute/gcp/gke.goimplementingcompute.Provider - Create
internal/adaptor/database/gcp/cloudsql.goimplementingdatabase.Provider - Create
internal/adaptor/ingress/gcp/traefik_gke.goimplementingingress.Provider - Wire the new adaptors into
cmd/wbsp/main.gounder a--provider gcpcase
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 namespaceHow 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
└──────────┘| Status | Meaning |
|---|---|
pending | Registered but not yet started |
deploying | Container and resources being provisioned |
running | Application is up, serving traffic from a container |
dev | Deployed to a type: dev destination — only data services run in containers; the app runs from the developer's IDE |
failed | Deployment or startup failed |
removing | Being torn down |
removed | Fully cleaned up |
Key transitions:
- running → dev:
devis a deployment type, not a runtime toggle. Deploying an app to atype: devdestination (wbsp deploy --destination <dev>) brings up only the data services (PostgreSQL/Redis) in containers and writes a.envnext 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 cloudtypesuch asaws) withwbsp 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 localreturns 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.
| Type | What runs locally | Typical use |
|---|---|---|
compose | the whole app stack in containers (app + per-app PostgreSQL/Redis) | production parity |
dev | only the data services in containers; the app runs from the developer's IDE | live-reload development |
standalone | the app and its data services inside a single ephemeral container | throwaway 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.
| Service | Conventional | Project 041 | Published |
|---|---|---|---|
| Web app | 3000 | 041 | 30041 |
| PostgreSQL | 5432 | 041 | 54041 |
| Redis | 6379 | 041 | 63041 |
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-platformThe 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
devor 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 acomposestack 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
awsdestination (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 Component | AWS Equivalent | Adaptor |
|---|---|---|
| Docker Compose | EKS (managed Kubernetes) | cloud/aws/provisioner.go |
docker run | K8s Deployment + Service | compute/aws/k8s.go |
| Docker image (local) | ECR repository | compute/aws/ecr.go |
| PostgreSQL container | RDS PostgreSQL (managed) | database/aws/rds.go |
| Traefik (Docker labels) | Traefik IngressRoute CRDs | ingress/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
traefiknamespace (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
- Ingress from the
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:
- Creates the ECR repository (idempotent)
- Logs in to ECR via
aws ecr get-login-password - Builds the Docker image with
--platform linux/amd64(required for EKS AMD64 nodes) - 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:
- Lists all deployed applications
- Warns the operator and prompts for confirmation
- Removes each application (deletes namespaces, cleans up ECR images)
- Deletes all
wbsp-*ECR repositories - Runs
terraform destroyto tear down infrastructure
Differences from a local-machine deployment
| Aspect | Local-machine types | AWS |
|---|---|---|
| Container runtime | Docker on the developer's machine | Kubernetes pods on EKS |
| Image registry | the local Docker daemon | ECR (variant-derived repositories) |
| Isolation | one Compose project per app | per-app K8s namespace + NetworkPolicies |
| Database | a per-app PostgreSQL container | Amazon RDS (managed, backed up) |
| Redis | a per-app Redis container | ElastiCache (cache) or MemoryDB (durable) |
| Ingress | none — ports published on the host | Traefik IngressRoute CRDs behind an ALB/NLB |
| TLS | none | ACME / ACM certificates |
| DNS | none needed (localhost:<derived port>) | operator-managed records to the load balancer |
| Scaling | single instance | Kubernetes HPA / node autoscaling |
| Secrets | .env.<destination> read at deploy | resolved into a protected Secret object |
| State | Terraform not involved | S3 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.
| Mode | Destination | Compute | Database | Lifetime |
|---|---|---|---|---|
normal (default) | type: aws | EKS pods | shared RDS | persistent |
on-demand | type: aws, mode: on-demand | Lambda, VPC-attached | shared RDS | persistent, scale-to-zero |
demo | the reserved demo destination | ephemeral EKS pod | bundled inside the instance | minutes (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.enginefield 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 namespacewbsp-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=trueis 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 method | Lambda 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-adapterIt 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
| Aspect | normal (EKS) | on-demand (Lambda) | demo (ephemeral EKS) |
|---|---|---|---|
| Compute | Kubernetes pods | Lambda, VPC-attached | one Kubernetes pod per instance |
| Database | RDS PostgreSQL | RDS PostgreSQL | real PostgreSQL bundled in the instance |
| Redis | ElastiCache / MemoryDB | ElastiCache / MemoryDB | bundled in the instance when declared |
| Seeded | no | no | yes, from a standard dump |
| Cold start | always warm | ~5–10s (VPC) | ~15–60s to first response |
| Max request duration | unlimited | 15 minutes | unlimited |
| Data persistence | persistent | persistent (RDS) | discarded with the instance |
| Auto-cleanup | no | no | yes (in-process reaper) |
| Ingress | Traefik IngressRoute | Traefik → Function URL | Traefik IngressRoute |
| Started by | wbsp deploy | wbsp deploy | a launch request, after a publish |
Google Cloud Platform (GCP)
Status: Planned. No adaptors implemented yet.
Planned Architecture
| Component | GCP Service |
|---|---|
| Container orchestration | Google Kubernetes Engine (GKE) |
| Database | Cloud SQL for PostgreSQL |
| Ingress | Traefik on GKE or Cloud Load Balancing |
| Infrastructure as Code | Terraform |
Adaptors Required
internal/adaptor/cloud/gcp/provisioner.go— GKE cluster via Terraforminternal/adaptor/database/gcp/cloudsql.go— Cloud SQL database and user managementinternal/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
| Component | Azure Service |
|---|---|
| Container orchestration | Azure Kubernetes Service (AKS) |
| Database | Azure Database for PostgreSQL (Flexible Server) |
| Ingress | Traefik on AKS or Azure Application Gateway |
| Infrastructure as Code | Terraform |
Adaptors Required
internal/adaptor/cloud/azure/provisioner.go— AKS cluster via Terraforminternal/adaptor/database/azure/pgflex.go— Azure PostgreSQL Flexible Server managementinternal/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
| Component | Vercel Equivalent |
|---|---|
| Container orchestration | Vercel Deployments (serverless functions + static) |
| Database | Vercel Postgres (Neon) or external provider |
| Ingress | Vercel Edge Network (automatic) |
| Infrastructure as Code | Vercel CLI / API |
Adaptors Required
internal/adaptor/cloud/vercel/provisioner.go— project creation and deployment via Vercel APIinternal/adaptor/database/vercel/neon.go— Vercel Postgres (Neon) database provisioninginternal/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 withvercel dev.
DigitalOcean
Status: Planned. No adaptors implemented yet.
Planned Architecture
| Component | DigitalOcean Service |
|---|---|
| Container orchestration | DigitalOcean Kubernetes (DOKS) |
| Database | Managed PostgreSQL |
| Ingress | Traefik on DOKS or DigitalOcean Load Balancer |
| Infrastructure as Code | Terraform |
Adaptors Required
internal/adaptor/cloud/digitalocean/provisioner.go— DOKS cluster via Terraforminternal/adaptor/database/digitalocean/managed_pg.go— Managed PostgreSQL database and user managementinternal/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.
| Capability | Local-machine | AWS (normal) | AWS (on-demand) | AWS (demo) | GCP | Azure | Vercel | DigitalOcean |
|---|---|---|---|---|---|---|---|---|
| Status | Complete | Complete | Complete | Complete | Planned | Planned | Planned | Planned |
| Provisioning | none — self-contained | Terraform → EKS | shared AWS platform | shared AWS platform | Terraform → GKE | Terraform → AKS | Vercel API | Terraform → DOKS |
| Container runtime | Docker | Kubernetes (EKS) | Lambda (VPC-attached) | Kubernetes (EKS), one pod per instance | Kubernetes (GKE) | Kubernetes (AKS) | Serverless | Kubernetes (DOKS) |
| Database | per-app Postgres container | RDS PostgreSQL | RDS PostgreSQL | real Postgres bundled in the instance | Cloud SQL | Azure Postgres | Vercel Postgres | Managed Postgres |
| Ingress | none (host ports) | Traefik (K8s) | Traefik → Function URL | Traefik (K8s) | Traefik (K8s) | Traefik (K8s) | Edge Network | Traefik (K8s) |
| TLS | No | Yes (ACME) | Yes (ACME) | Yes (wildcard) | Yes (managed) | Yes (managed) | Yes (automatic) | Yes (Let's Encrypt) |
| Autoscaling | No | Yes (HPA) | Yes (automatic) | No — one pod, then reaped | Yes (HPA) | Yes (HPA) | Yes (automatic) | Yes (HPA) |
| Cost | Free | $$$ | $ (scale-to-zero) | $ (minutes per instance) | $$$ | $$$ | $$ | $$ |
| Setup time | ~1 minute | 15–25 min | <90s (deploy) | <90s (publish); ~15–60s (launch) | 15–25 min | 15–25 min | ~2 min | 10–15 min |
| Docker images | local daemon | ECR | ECR + Web Adapter | ECR (self-contained image) | GCR / Artifact Registry | ACR | N/A (source) | DOCR / registry |
| Auto-cleanup | No | No | No | Yes (in-process reaper) | No | No | No | No |
| Data persistence | Persistent | Persistent | Persistent (RDS) | Discarded with the instance | Persistent | Persistent | Persistent | Persistent |
| Started by | wbsp deploy | wbsp deploy | wbsp deploy | a launch request, after a publish | wbsp deploy | wbsp deploy | wbsp deploy | wbsp 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.