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 Environment (Docker)
  8. AWS Environment
  9. AWS 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 two interfaces:

  • wbsp CLI — 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) 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 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):

  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

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 network

Note: 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
└──────────┘
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-app 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-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 machine

Platform Components

ComponentImageHost PortPurpose
Traefiktraefik:v2.118080 (ingress), 8081 (dashboard)Routes HTTP traffic to application containers by domain name. Discovers containers via Docker labels.
PostgreSQLpostgres:16-alpine5432Shared database server. Each application gets its own database and user, created on demand. Data persists in a Docker volume (pgdata).
WBSP APIwbsp-api:latest (built from repo)9090REST 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=8080

When 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 wbsp Docker network
  • Labelled with wbsp.tenant and wbsp.app for 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.local

Traffic 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 data

The 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.replicas is ignored
  • No load balancing across instances — single container per app

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 Local

AspectLocalAWS
Container runtimeDocker directlyKubernetes pods on EKS
Image registryLocal Docker imagesECR (wbsp-<app> repos)
Namespace isolationShared Docker networkPer-app K8s namespace with NetworkPolicies
DatabaseShared Postgres containerAmazon RDS (managed, backed up)
IngressTraefik via Docker labelsTraefik via IngressRoute CRDs
TLSNoneACM certificates via ALB
DNS/etc/hosts manual entriesRoute 53 or external DNS
ScalingSingle instanceKubernetes 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 stateN/AS3 backend with DynamoDB locking

AWS Lambda Modes

Status: Implemented. Two Lambda-based aws deployment 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.

ModeDestinationDatabaseVPCUse Case
Lambdatype: aws, mode: on-demandShared RDS (same as mode: normal)Yes (VPC-attached)Production serverless workloads with scale-to-zero
AWS Sampletype: aws, mode: demoEmbedded (SQLite or PostgreSQL)NoEphemeral 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 MethodLambda 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-adapter

The 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:

  1. Creates the Lambda function and obtains its Function URL
  2. Creates a Traefik IngressRoute that maps Host(domain) to the Function URL as a backend service
  3. 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 via modernc.org/sqlite. The database file is stored in /tmp on the Lambda execution environment. Zero configuration, fastest startup.
  • postgres — Embedded PostgreSQL via fergusstrange/embedded-postgres. Full PostgreSQL compatibility in a single process, also using /tmp storage.

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:

  1. On deploy: The deployer generates an 8-character hex instance token (e.g., a1b2c3d4) via GenerateInstanceToken(). The function is named wbsp-<tenant>-<app>-<token>.
  2. Schedule creation: ScheduleCleanup() creates a one-shot EventBridge Scheduler rule that fires after the timeout period. The schedule expression uses at(YYYY-MM-DDTHH:MM:SS) for a single execution.
  3. Cleanup target: The schedule invokes a wbsp-demo-cleanup Lambda function with a JSON payload containing the function name to delete.
  4. Self-deleting schedule: The schedule is created with ActionAfterCompletion: DELETE, so it removes itself after firing.
  5. Manual removal: If the user runs wbsp-app remove before the timeout, Stop() calls CancelCleanup() 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

AspectEKS (mode: normal)Lambda (mode: on-demand)Sample (mode: demo)
ComputeKubernetes podsLambda (VPC-attached)Lambda (no VPC)
DatabaseRDS PostgreSQLRDS PostgreSQLEmbedded (SQLite/PG)
Cold startAlways warm~5–10s (VPC)<8s
ScalingHPA / node scalingAutomatic (concurrent invocations)Automatic
Max request durationUnlimited15 minutes15 minutes
Data persistencePersistentPersistent (RDS)Ephemeral (/tmp)
Auto-cleanupNoNoYes (EventBridge)
Image buildStandard DockerfileDockerfile + Web AdapterDockerfile + Web Adapter + sample data
IngressTraefik IngressRouteTraefik → Function URLTraefik → Function URL

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

CapabilityLocal (Docker)AWSLambdaAWS SampleGCPAzureVercelDigitalOcean
StatusCompleteCompleteCompleteCompletePlannedPlannedPlannedPlanned
ProvisioningDocker ComposeTerraform → EKSShared AWS platformShared AWS platformTerraform → GKETerraform → AKSVercel APITerraform → DOKS
Container runtimeDockerKubernetes (EKS)Lambda (VPC)Lambda (no VPC)Kubernetes (GKE)Kubernetes (AKS)ServerlessKubernetes (DOKS)
DatabasePostgres containerRDS PostgreSQLRDS PostgreSQLEmbedded (SQLite/PG)Cloud SQLAzure PostgresVercel PostgresManaged Postgres
IngressTraefik (Docker)Traefik (K8s)Traefik → Function URLTraefik → Function URLTraefik (K8s)Traefik (K8s)Edge NetworkTraefik (K8s)
TLSNoYes (ACM)Yes (ACM)Yes (ACM)Yes (managed)Yes (managed)Yes (automatic)Yes (Let's Encrypt)
AutoscalingNoYes (HPA)Yes (automatic)Yes (automatic)Yes (HPA)Yes (HPA)Yes (automatic)Yes (HPA)
Local-machine dev (type: dev)YesNoNoNoNoNoNoNo
CostFree$$$$ (scale-to-zero)$ (ephemeral)$$$$$$$$$$
Setup time~1 minute15–25 min<90s (deploy)<90s (deploy)15–25 min15–25 min~2 min10–15 min
Docker imagesLocalECR / registryECR + Web AdapterECR + Web AdapterGCR / Artifact RegistryACRN/A (source)DOCR / registry
Auto-cleanupNoNoNoYes (EventBridge)NoNoNoNo
Data persistencePersistentPersistentPersistent (RDS)Ephemeral (/tmp)PersistentPersistentPersistentPersistent

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.