Skip to content

Automated tenant onboarding and offboarding

Connect the two planes. When a system admin registers a tenant in the control plane, the application plane provisions it automatically: it creates the tenant’s admin user and reports the tenant’s configuration back. Deleting the registration deprovisions it: users, group and data are removed.

Until now, tenants existed only because we created users by hand with dev-create-tenant-user.sh.

Onboarding a tenant: control plane, EventBridge, provisioning job and tenant stack

The provisioning job is SBT’s ProvisioningScriptJob in the bootstrap stack: Step Functions runs server/scripts/provision-tenant.sh in CodeBuild with tenantId, tier, tenantName and email as environment variables. The script reads the pooled stack’s outputs, creates tenant-admin-<tenantId> with custom:tenantId, custom:userRole and custom:tenantTier, creates the group <tenantId> and adds the admin to it. It exports tenantConfig={userPoolId, appClientId, apiGatewayUrl}, tenantStatus=Complete and registrationStatus=Complete, which become the provisionSuccess event. The control plane receives it through an EventBridge API destination (PATCH /tenant-registrations/{id}, using the machine OAuth client) and serves the config publicly at GET /tenant-config/{tenantName}.

Offboarding is the mirror image: DELETE /tenant-registrations/{id} → offboardingRequest → deprovision-tenant.sh deletes the tenant’s users and group, deletes every item in shards <tenantId>-1..9 of the product and order tables → deprovisionSuccess → registration status Deleted.

Why a CodeBuild job running a bash script?

Section titled “Why a CodeBuild job running a bash script?”

SBT’s “core application plane” deliberately makes provisioning just a script. That way the same mechanism can do anything: create a user (pooled tiers, here) or run cdk deploy for a whole new stack (Platinum, step 09). CodeBuild provides a long-running environment with the AWS CLI, Node, Python and Docker. Step Functions wires it to EventBridge and turns the script’s exported env vars into the outgoing event.

Tier Provisioning does Tenant uses
Basic / Standard / Premium Admin user + group in the existing pooled user pool pooled API, pooled tables
Platinum (step 09) deploys a dedicated stack first its own stack

The tenant admin gets a Cognito email with a temporary password, and the first sign-in must set a new one.

The original reference solution gives both CodeBuild jobs "Action": "*". Here each job gets only what its script needs: read the tenant stacks’ outputs, the Cognito admin calls on user pools, and for deprovisioning Query/DeleteItem on tenant-template tables. Step 09 widens provisioning, because deploying a CDK stack genuinely needs broad permissions.

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

If you deploy stacks one at a time, deploy ControlPlaneStack before serverless-saas-bootstrap. The bootstrap stack imports the event bus ARN that the control plane now exports. Otherwise you get No export named ControlPlaneStack:ExportsOutput...EventBus....

Terminal window
ADMIN_PASSWORD='Passw0rd!Saas' ./scripts/onboard-tenant.sh acme acme-admin@example.com BASIC

Result from the test deployment, about 2 minutes later:

{ "tenantName": "acme", "tier": "BASIC", "tenantStatus": "Complete",
"tenantId": "e2f1f038-254c-4336-8ded-9e62a83eaf61",
"tenantConfig": "{\"userPoolId\":\"us-east-1_a1M1hAn8y\",\"appClientId\":\"q33c...\",\"apiGatewayUrl\":\"https://0xlsgzuyxh.execute-api.us-east-1.amazonaws.com/prod/\"}" }

The public lookup the SaaS web app will use (step 11). No token is needed:

Terminal window
curl -s ${CONTROL_PLANE_API}tenant-config/acme
# {"userPoolId":"us-east-1_a1M1hAn8y","appClientId":"q33c...","apiGatewayUrl":"https://0xlsgzuyxh.../prod/"}

The provisioned admin tenant-admin-<tenantId> has status FORCE_CHANGE_PASSWORD and attributes custom:tenantId=<tenantId>, custom:userRole=TenantAdmin, custom:tenantTier=BASIC. After setting a password (or using the emailed one), they can use the API right away:

Call Result
/whoami tenantId: e2f1..., tenantTier: BASIC, userRole: TenantAdmin
create a product, list ["Acme Anvil"]
/users ["tenant-admin-e2f1..."]

Offboard:

Terminal window
ADMIN_PASSWORD='Passw0rd!Saas' ./scripts/offboard-tenant.sh acme
# {"registrationStatus":"Deleted","sbtaws_active":false,...}

Afterwards the user and the group are gone (UserNotFoundException, Group not found), and the product and order tables hold no e2f1...-* shards (only the dev tenants tenant-a and tenant-b remain).

Events on the bus (/aws/events/serverless-saas-event-bus-watcher):

sbt_aws_onboardingRequest acme
sbt_aws_provisionSuccess 326b44d6-...
sbt_aws_offboardingRequest acme
sbt_aws_deprovisionSuccess 326b44d6-...

SBT 0.9.5 keeps serving GET /tenant-config/acme after offboarding, because the tenant record is only marked inactive. It’s harmless here since the tenant’s users no longer exist, but a production system should filter inactive tenants out of that lookup.

Step 09 adds the Platinum tier: provisioning deploys a dedicated copy of the tenant template stack for the tenant.