Skip to content

Control plane (SBT)

Create the CDK app and deploy the control plane: the part of a SaaS system that the SaaS provider (you) runs for itself. It is shared by every tenant and holds no tenant business data. It knows which tenants exist, how to reach each tenant’s resources and who the provider’s admins are.

Control plane Application plane
Who uses it SaaS provider staff (system admins) Tenants’ users
What it does Register/offboard tenants, store tenant config, manage admin users The actual product (products, orders)
Users live in One Cognito pool for admins Tenant Cognito pool(s) (step 03)
In this repo ControlPlaneStack (this step) Steps 02–09

The two halves talk only through EventBridge events. The control plane emits “onboarding request” and the application plane answers “provision success” (step 08).

SaaS Builder Toolkit is an AWS Labs CDK library that packages the control plane as two constructs:

  • CognitoAuth: a Cognito user pool for system admins, with a user app client (for humans) and a machine client (for service-to-service calls). It creates the first admin user (admin) from the email you pass in.
  • ControlPlane: an HTTP API with these routes, each backed by a Python Lambda and DynamoDB:
Route Purpose
POST/GET/PATCH/DELETE /tenant-registrations Start onboarding or offboarding a tenant. Emits an event.
GET/PUT/DELETE /tenants Tenant records (name, tier, status, config)
GET /tenant-config/{tenantName} Public lookup the SaaS web app uses to find a tenant’s user pool and API URL (step 11)
GET/POST/PUT/DELETE /users System admin users

It also creates the EventBridge bus that connects both planes.

Two settings differ from SBT’s defaults:

  • setAPIGWScopes: false: API routes accept any valid token from the admin pool, without OAuth scopes. The original reference solution does this so the admin web app can call the API with a plain ID token. Use scopes in production.
  • enableAdvancedSecurityMode: false: Cognito advanced security is a paid feature.

Docker must be running (CDK uses it to bundle SBT’s Python Lambdas).

Terminal window
./scripts/deploy.sh you@example.com

Outputs to note:

ControlPlaneStack.ControlPlaneIdpUserPoolId = us-east-1_xxxx
ControlPlaneStack.ControlPlaneIdpClientId = xxxx
ControlPlaneStack.controlPlaneAPIEndpoint = https://xxxx.execute-api.us-east-1.amazonaws.com/
ControlPlaneStack.eventBridgeArn = arn:aws:events:...:event-bus/...

Cognito emails a temporary password to the admin email. You can use it, or set a permanent password directly (the next section does this).

Terminal window
# Set a permanent password for "admin" and get an ID token
TOKEN=$(./scripts/admin-token.sh 'Passw0rd!Saas' --set-password)
API=$(aws cloudformation describe-stacks --stack-name ControlPlaneStack \
--query "Stacks[0].Outputs[?OutputKey=='controlPlaneAPIEndpoint'].OutputValue" --output text)
curl -s -o /dev/null -w '%{http_code}\n' ${API}tenants # 401: no token
curl -s -H "Authorization: Bearer $TOKEN" ${API}tenants # {"data":[]}
curl -s -H "Authorization: Bearer $TOKEN" ${API}users # the admin user, role SystemAdmin

Watch an onboarding event flow over the bus. Nothing provisions the tenant yet, because the application plane arrives in step 08:

Terminal window
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' ${API}tenant-registrations \
-d '{"tenantData":{"tenantName":"demo","email":"demo@example.com","tier":"BASIC"},"tenantRegistrationData":{"registrationStatus":"In progress"}}'
aws logs filter-log-events --log-group-name /aws/events/serverless-saas-event-bus-watcher --query 'events[].message' --output text

Result from the test deployment:

{"detail-type":"sbt_aws_onboardingRequest","source":"sbt.control.plane",
"detail":{"registrationStatus":"In progress","tenantName":"demo","email":"demo@example.com","tier":"BASIC",
"tenantRegistrationId":"9b4e...","tenantId":"c2aa..."}}

Every field in detail becomes an input for the provisioning job in step 08.

Clean up the demo registration with curl -X DELETE -H "Authorization: Bearer $TOKEN" ${API}tenant-registrations/<tenantRegistrationId>.

Step 02 adds the bootstrap stack: shared resources the application plane needs before any tenant exists.