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 Environment (Docker)
- AWS Environment
- AWS 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 two interfaces:
wbspCLI — a Go binary (built with Cobra) that operators and application creators run on their local machine. It provisions the platform, deploys apps, checks health, and manages the full lifecycle.- REST API — a Go HTTP server (built with Chi) that runs inside the platform and provides the same capabilities over HTTP. The CLI talks to the API for remote operations; for local operations the CLI drives Docker directly.
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 running inside the platform (as a Docker container locally, as a service on cloud). Provides endpoints for deploy, list, status, remove, and health. The CLI uses this for remote operations.
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
When a user visits http://app-one.local:8080/:
Browser
│
▼
Traefik (ingress) Listens on host port 8080
│
│ Matches Host header → app-one.local
│ Routes to backend container
│
▼
┌─────────────────────┐
│ app-one container │ Container-backed deployment (compose/standalone):
│ (listening on 8080) │ traffic goes to the app container on port 8080
└─────────────────────┘ inside the Docker networkNote: this Traefik request flow is the AWS path (and the retired v1 local
target). The feature-063 local-machine types do not use Traefik at all:
compose and standalone publish the app directly on a host port, and dev
runs no app container — the app runs from your IDE and talks to PostgreSQL/Redis
directly on host-published ports (see Local Environment).
Container-backed deployment
Traefik discovers the app container via Docker labels. The container is on the wbsp Docker network. Traefik routes by matching the Host header to the router rule on the container's labels, then forwards to the container's IP on port 8080.
type: dev destinations
A type: dev destination brings up only the data services (PostgreSQL/Redis) in containers and writes a .env next to the app with discrete DATABASE_*/REDIS_* values pointing at localhost on derived ports. There is no app container and no Traefik proxy — you run the app yourself from the IDE (source .env && <run your app>).
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-app 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-app deploy. Database preserved. - failed → dev or failed → deploying: Retry from failure.
Local Environment (Docker)
The local environment runs entirely on Docker Desktop using Docker Compose. It is the primary development environment and provides a functionally complete replica of production without the operational overhead of cloud infrastructure.
Architecture
┌──────────────────────── Docker Desktop ────────────────────────────┐
│ │
│ ┌────────────────────── wbsp network ──────────────────────────┐ │
│ │ │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
│ │ │ Traefik │ │ PostgreSQL │ │ WBSP API │ │ │
│ │ │ v2.11 │ │ 16-alpine │ │ (Go + Chi) │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ :80 → :8080 │ │ :5432→:5432 │ │ :9090 → :9090 │ │ │
│ │ │ :8080→:8081 │ │ │ │ │ │ │
│ │ │ (dashboard) │ │ pgdata vol │ │ Docker socket │ │ │
│ │ └──────────────┘ └──────────────┘ └──────────────────┘ │ │
│ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ app-one │ │ app-two │ │ app-three│ │ │
│ │ │ :8080 │ │ :8080 │ │ :8080 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────┘
▲ ▲ ▲
:8080 (ingress) :5432 (DB) :9090 (API)
│ │ │
─────┴────────────────────┴────────────────────┴──── Host machinePlatform Components
| Component | Image | Host Port | Purpose |
|---|---|---|---|
| Traefik | traefik:v2.11 | 8080 (ingress), 8081 (dashboard) | Routes HTTP traffic to application containers by domain name. Discovers containers via Docker labels. |
| PostgreSQL | postgres:16-alpine | 5432 | Shared database server. Each application gets its own database and user, created on demand. Data persists in a Docker volume (pgdata). |
| WBSP API | wbsp-api:latest (built from repo) | 9090 | REST API for application deployment and platform health. Has access to the Docker socket to manage containers. |
How Routing Works (Local)
Traefik runs with the Docker provider enabled (--providers.docker=true) and exposedbydefault=false. Application containers opt in to routing via Docker labels:
traefik.enable=true
traefik.http.routers.<name>.rule=Host(`app-one.local`)
traefik.http.routers.<name>.entrypoints=web
traefik.http.services.<name>.loadbalancer.server.port=8080When an application is deployed, the deployer starts a Docker container with these labels on the wbsp network. Traefik detects the new container automatically and begins routing traffic for that domain.
This applies to container-backed deployments. A type: dev destination does not register any Traefik route at all — only the data services run in containers, and you reach the app directly from your IDE.
How Database Provisioning Works (Local)
The local database provider connects to the shared PostgreSQL container as wbsp_admin and runs:
CREATE DATABASE <tenant>_<app_name>;
CREATE USER <user> WITH PASSWORD '<password>';
GRANT ALL PRIVILEGES ON DATABASE <tenant>_<app_name> TO <user>;These operations are idempotent — re-provisioning an existing database succeeds silently. The database persists across redeploys (and across wbsp-app stop), and is dropped only on explicit wbsp-app remove.
How Container Management Works (Local)
The deployer shells out to docker run and docker stop/docker rm directly. Containers are:
- Named
<tenant>-<app-name>(e.g.,test-app-one) - Connected to the
wbspDocker network - Labelled with
wbsp.tenantandwbsp.appfor identification - Given environment variables from the config file plus database credentials
DNS (Local)
For local development, you must add entries to /etc/hosts to map application domains to localhost:
127.0.0.1 app-one.local
127.0.0.1 app-two.localTraffic hits Traefik on localhost:8080, which matches the Host header and routes to the correct container.
type: dev Destinations (Local)
A type: dev destination runs only the data services (per-app PostgreSQL/Redis) in containers and writes a .env next to the app with discrete DATABASE_*/REDIS_* values pointing at localhost on derived ports. There is no app container and no Traefik route — you run the app from your IDE with live reload:
wbsp-app deploy --destination dev # brings up DB/Redis, writes .env
source .env && <run your app from the IDE>
wbsp-app stop <app> --tenant <tenant> # stop, preserving data
wbsp-app remove <app> --tenant <tenant> # tear down, deleting dataThe two other local-machine types — compose (whole app stack in containers, production-parity) and standalone (app + DB + Redis in one ephemeral container) — run the app in a container instead. All three expose the same discrete DATABASE_*/REDIS_* env contract as aws and use host-published ports derived from projectNo. See the Application Creator Guide for usage details.
Limitations (Local)
- No TLS — all traffic is plain HTTP
- No autoscaling —
resources.replicasis ignored - No load balancing across instances — single container per app
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 Local
| Aspect | Local | AWS |
|---|---|---|
| Container runtime | Docker directly | Kubernetes pods on EKS |
| Image registry | Local Docker images | ECR (wbsp-<app> repos) |
| Namespace isolation | Shared Docker network | Per-app K8s namespace with NetworkPolicies |
| Database | Shared Postgres container | Amazon RDS (managed, backed up) |
| Ingress | Traefik via Docker labels | Traefik via IngressRoute CRDs |
| TLS | None | ACM certificates via ALB |
| DNS | /etc/hosts manual entries | Route 53 or external DNS |
| Scaling | Single instance | Kubernetes HPA / node autoscaling |
Local-machine dev (type: dev) | Supported (data services in containers; app from the IDE) | Not applicable (use a local-machine destination for development) |
| Terraform state | N/A | S3 backend with DynamoDB locking |
AWS Lambda Modes
Status: Implemented. Two Lambda-based
awsdeployment modes that share the existing AWS platform infrastructure.
The platform provides two additional aws destination modes that run applications on Lambda instead of EKS. Both use the same compute.Provider interface as the EKS path, so all CLI commands (deploy, status, logs, remove) work identically.
| Mode | Destination | Database | VPC | Use Case |
|---|---|---|---|---|
| Lambda | type: aws, mode: on-demand | Shared RDS (same as mode: normal) | Yes (VPC-attached) | Production serverless workloads with scale-to-zero |
| AWS Sample | type: aws, mode: demo | Embedded (SQLite or PostgreSQL) | No | Ephemeral demo instances with auto-cleanup |
Lambda Compute Provider
The Lambda compute provider (internal/adaptor/compute/lambda/lambda.go) implements compute.Provider for both modes. The Provider struct holds AWS SDK clients for Lambda, CloudWatch Logs, and ECR, plus VPC configuration (security group, subnets) used only by the on-demand mode.
Each method maps to a Lambda operation:
| Interface Method | Lambda Operation |
|---|---|
Build() | Build container image with Lambda Web Adapter, push to ECR |
Start() | Create Lambda function + Function URL |
Stop() | Delete function, Function URL, CloudWatch log group, and cleanup schedule |
Logs() | Read from CloudWatch Logs (/aws/lambda/<function-name>) |
List() | List functions filtered by wbsp-* tags |
Status() | Get function state (Active, Pending, Failed) |
Lambda Architecture
┌─────────────────────── AWS Region ────────────────────────────────┐
│ │
│ ┌─── VPC ──────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ┌── EKS Cluster ────────────────────────────────────────┐ │ │
│ │ │ Traefik (ingress controller) │ │ │
│ │ │ │ │ │ │
│ │ │ │ IngressRoute: Host(`app.example.com`) │ │ │
│ │ │ │ → service backend: Function URL │ │ │
│ │ └────┼──────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌── Lambda (mode: on-demand) ────────────────────────────┐ │ │
│ │ │ VPC-attached function │ │ │
│ │ │ Container image from ECR + Lambda Web Adapter │ │ │
│ │ │ Function URL (IAM auth: NONE) │ │ │
│ │ │ ──► RDS PostgreSQL (private subnets) │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── Lambda (mode: demo) ──────────────────────────────────────┐ │
│ │ Non-VPC function (fast cold start) │ │
│ │ Container image from ECR + Lambda Web Adapter │ │
│ │ Function URL (IAM auth: NONE) │ │
│ │ Embedded SQLite in /tmp (ephemeral) │ │
│ │ │ │
│ │ EventBridge Scheduler ──► wbsp-demo-cleanup Lambda │ │
│ │ (fires after timeout, deletes function + schedule) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ ┌── ECR ────────────────────────────────────────────────────────┐ │
│ │ wbsp-<app> repositories (shared with EKS target) │ │
│ └───────────────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘Lambda Web Adapter Image Build
The Lambda compute provider reuses the existing ECR infrastructure. The Build() method (internal/adaptor/compute/lambda/ecr.go) extends the application's Dockerfile by appending a COPY instruction that adds the AWS Lambda Web Adapter extension:
# Appended to the application's Dockerfile
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:1.0.0 /lambda-adapter /opt/extensions/lambda-adapterThe web adapter intercepts Lambda invoke events, translates them to HTTP requests, and forwards them to the application's HTTP port — the application code requires no changes. For mode: demo, sample data files can also be embedded into the image at /opt/sample-data/.
The generated Dockerfile.lambda is built with --platform linux/amd64 and pushed to the same ECR registry used by the EKS path.
Function URL Routing Through Traefik
Lambda functions are exposed via Function URLs (public, AuthType: NONE). Rather than exposing Function URLs directly, traffic is routed through Traefik on EKS using IngressRoute CRDs, the same routing mechanism used by the EKS target.
The deployer:
- Creates the Lambda function and obtains its Function URL
- Creates a Traefik IngressRoute that maps
Host(domain)to the Function URL as a backend service - External traffic hits the load balancer → Traefik → Function URL → Lambda
This keeps a single ingress point for all applications regardless of compute backend, and allows domain-based routing, TLS termination, and path-prefix middleware to work identically across EKS and Lambda workloads.
VPC-Attached Lambda (mode: on-demand)
The on-demand mode attaches the Lambda function to the platform VPC by setting VpcConfig with the private subnet IDs and a security group. This gives the function network access to RDS in the same VPC. The database provider is the same RDS provider used by mode: normal, so applications get the same managed PostgreSQL with SSL, per-app databases, and credential rotation.
The trade-off is a slower cold start (VPC attachment adds a few seconds) in exchange for full database access. This mode is intended for production serverless workloads that need a shared, persistent database.
Non-VPC Lambda (mode: demo)
The demo mode creates Lambda functions outside the VPC for faster cold starts. Since there is no VPC attachment, the function cannot reach RDS. Instead, the platform uses the embedded database provider (internal/adaptor/database/embedded/embedded.go).
The embedded provider supports two engines:
lightweight(default) — SQLite viamodernc.org/sqlite. The database file is stored in/tmpon the Lambda execution environment. Zero configuration, fastest startup.postgres— Embedded PostgreSQL viafergusstrange/embedded-postgres. Full PostgreSQL compatibility in a single process, also using/tmpstorage.
Both engines are ephemeral — data is lost when the Lambda execution environment is recycled. This is by design: mode: demo is for demos and previews, not persistent workloads.
EventBridge Scheduler Cleanup
Demo instances deployed with mode: demo are automatically cleaned up after a configurable timeout. The cleanup flow (internal/adaptor/compute/lambda/cleanup.go) works as follows:
- On deploy: The deployer generates an 8-character hex instance token (e.g.,
a1b2c3d4) viaGenerateInstanceToken(). The function is namedwbsp-<tenant>-<app>-<token>. - Schedule creation:
ScheduleCleanup()creates a one-shot EventBridge Scheduler rule that fires after the timeout period. The schedule expression usesat(YYYY-MM-DDTHH:MM:SS)for a single execution. - Cleanup target: The schedule invokes a
wbsp-demo-cleanupLambda function with a JSON payload containing the function name to delete. - Self-deleting schedule: The schedule is created with
ActionAfterCompletion: DELETE, so it removes itself after firing. - Manual removal: If the user runs
wbsp-app removebefore the timeout,Stop()callsCancelCleanup()to delete the schedule, then deletes the function normally.
The unguessable token appears as a subdomain in the demo URL (e.g., http://a3f8b2c1.my-app.demo.wbsp.io/), preventing URL enumeration while allowing the application to run at the root path /.
Differences from the EKS Path
| Aspect | EKS (mode: normal) | Lambda (mode: on-demand) | Sample (mode: demo) |
|---|---|---|---|
| Compute | Kubernetes pods | Lambda (VPC-attached) | Lambda (no VPC) |
| Database | RDS PostgreSQL | RDS PostgreSQL | Embedded (SQLite/PG) |
| Cold start | Always warm | ~5–10s (VPC) | <8s |
| Scaling | HPA / node scaling | Automatic (concurrent invocations) | Automatic |
| Max request duration | Unlimited | 15 minutes | 15 minutes |
| Data persistence | Persistent | Persistent (RDS) | Ephemeral (/tmp) |
| Auto-cleanup | No | No | Yes (EventBridge) |
| Image build | Standard Dockerfile | Dockerfile + Web Adapter | Dockerfile + Web Adapter + sample data |
| Ingress | Traefik IngressRoute | Traefik → Function URL | Traefik → Function URL |
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
| Capability | Local (Docker) | AWS | Lambda | AWS Sample | GCP | Azure | Vercel | DigitalOcean |
|---|---|---|---|---|---|---|---|---|
| Status | Complete | Complete | Complete | Complete | Planned | Planned | Planned | Planned |
| Provisioning | Docker Compose | Terraform → EKS | Shared AWS platform | Shared AWS platform | Terraform → GKE | Terraform → AKS | Vercel API | Terraform → DOKS |
| Container runtime | Docker | Kubernetes (EKS) | Lambda (VPC) | Lambda (no VPC) | Kubernetes (GKE) | Kubernetes (AKS) | Serverless | Kubernetes (DOKS) |
| Database | Postgres container | RDS PostgreSQL | RDS PostgreSQL | Embedded (SQLite/PG) | Cloud SQL | Azure Postgres | Vercel Postgres | Managed Postgres |
| Ingress | Traefik (Docker) | Traefik (K8s) | Traefik → Function URL | Traefik → Function URL | Traefik (K8s) | Traefik (K8s) | Edge Network | Traefik (K8s) |
| TLS | No | Yes (ACM) | Yes (ACM) | Yes (ACM) | Yes (managed) | Yes (managed) | Yes (automatic) | Yes (Let's Encrypt) |
| Autoscaling | No | Yes (HPA) | Yes (automatic) | Yes (automatic) | Yes (HPA) | Yes (HPA) | Yes (automatic) | Yes (HPA) |
Local-machine dev (type: dev) | Yes | No | No | No | No | No | No | No |
| Cost | Free | $$$ | $ (scale-to-zero) | $ (ephemeral) | $$$ | $$$ | $$ | $$ |
| Setup time | ~1 minute | 15–25 min | <90s (deploy) | <90s (deploy) | 15–25 min | 15–25 min | ~2 min | 10–15 min |
| Docker images | Local | ECR / registry | ECR + Web Adapter | ECR + Web Adapter | GCR / Artifact Registry | ACR | N/A (source) | DOCR / registry |
| Auto-cleanup | No | No | No | Yes (EventBridge) | No | No | No | No |
| Data persistence | Persistent | Persistent | Persistent (RDS) | Ephemeral (/tmp) | Persistent | Persistent | Persistent | Persistent |
The type: dev destination is a local-machine-only feature — it runs the data services in containers and lets you run the app from your IDE for live reload. In cloud environments, applications are always deployed as containers or serverless functions.