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 vs application plane
Section titled “Control plane vs application plane”| 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).
What SBT gives us
Section titled “What SBT gives us”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.
- server/cdk/bin/serverless-saas.ts: the CDK app entry point. Reads
CDK_PARAM_SYSTEM_ADMIN_EMAILand creates the stacks. - server/cdk/lib/control-plane-stack.ts:
CognitoAuth+ControlPlane, plus an EventBridge rule that copies every event on the bus to the CloudWatch log group/aws/events/serverless-saas-event-bus-watcher. That log is how you watch the two planes talk. - server/cdk/lib/destroy-policy-setter.ts: a CDK Aspect that makes every resource deletable on
cdk destroy. - scripts/deploy.sh: installs dependencies and deploys all stacks.
- scripts/admin-token.sh: signs in as the system admin and prints an ID token.
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.
Deploy
Section titled “Deploy”Docker must be running (CDK uses it to bundle SBT’s Python Lambdas).
./scripts/deploy.sh you@example.comOutputs to note:
ControlPlaneStack.ControlPlaneIdpUserPoolId = us-east-1_xxxxControlPlaneStack.ControlPlaneIdpClientId = xxxxControlPlaneStack.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).
Verify
Section titled “Verify”# Set a permanent password for "admin" and get an ID tokenTOKEN=$(./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 tokencurl -s -H "Authorization: Bearer $TOKEN" ${API}tenants # {"data":[]}curl -s -H "Authorization: Bearer $TOKEN" ${API}users # the admin user, role SystemAdminWatch an onboarding event flow over the bus. Nothing provisions the tenant yet, because the application plane arrives in step 08:
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 textResult 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.