Platinum tier: a dedicated (silo) stack per tenant
Offer a premium tier where the tenant shares nothing with other tenants: its own user pool, API, Lambdas and tables. Onboarding a Platinum tenant deploys a new copy of the tenant template stack. Offboarding deletes that stack.
This is the reference solution’s hybrid model: one pooled stack for most tenants, and silo stacks for those who pay for isolation, noisy-neighbour protection and dedicated throughput.
Pooled vs silo, from the same code
Section titled “Pooled vs silo, from the same code”| Pooled stack | Silo stack | |
|---|---|---|
CDK_PARAM_TENANT_ID |
pooled (default) |
the tenant’s id |
| User pool | shared by many tenants (one group each) | the tenant’s own |
| Tables | shared, isolated by LeadingKeys |
the tenant’s own |
| Lambdas reach DynamoDB with | authorizer’s tenant-scoped credentials (IS_POOLED_DEPLOY=true) |
their own role (IS_POOLED_DEPLOY=false) |
| Usage plans | Basic / Standard / Premium | Platinum (300 rps, 10,000/day) |
| Reserved concurrency | none, shares the account pool | 5 per function (CDK_PARAM_LAMBDA_RESERVE_CONCURRENCY) |
In a silo stack the authorizer still validates the token, still returns the tier’s API key and still vends scoped credentials, so the services’ code doesn’t branch on deployment model beyond tenant_data.get_table.
How provisioning deploys a stack
Section titled “How provisioning deploys a stack”scripts/deploy.shnow ends by running scripts/upload-source.sh, which zipsserver/(CDK app, Lambda code, job scripts) and uploads it to a versioned S3 bucket created by the bootstrap stack (serverless-saas-source-<account>-<region>). Each upload’s S3 version id identifies that code version (codeCommitIdin the mapping table, used by the pipeline in step 12).- On a
PLATINUMonboarding event, provision-tenant.sh downloads the newestsource.zip, runsnpm ciand then:Terminal window CDK_PARAM_TENANT_ID=$tenantId npx cdk deploy serverless-saas-tenant-template-$tenantId --exclusively--exclusivelydeploys only that stack, and the CDK CLI only bundles that stack’s assets. - Then it continues as for pooled tenants: create the tenant admin in the new stack’s user pool and export its
tenantConfig.
Two details:
- ARM64 build image. Our Lambdas are ARM64 and CDK bundles Python with Docker. On SBT’s default x86 CodeBuild image that would need emulation, so the provisioning job uses
aws/codebuild/amazonlinux-aarch64-standard:3.0(SBT runs jobs in privileged mode, so Docker is available). - Permissions without admin. The job doesn’t get
*.cdk deployassumes the CDK bootstrap roles (cdk-*-deploy-role,cdk-*-file-publishing-role), and CloudFormation itself runs ascdk-*-cfn-exec-role. The job only needssts:AssumeRoleoncdk-*plus read access to the source bucket.
Deprovisioning a Platinum tenant looks up its stack in the tenant mapping table and runs aws cloudformation delete-stack. The stack keeps using the cfn-exec role it was created with, so the job needs cloudformation:DeleteStack and iam:PassRole on that role. Deleting the stack also removes its mapping row (the custom resource’s onDelete).
Also in this step
Section titled “Also in this step”- Tier settings in one place: server/cdk/lib/tenant-template/tiers.ts. Each stack looks up only the API keys of the tiers it serves. Before this, a silo stack declared unused SSM parameters for the pooled keys, and CodeBuild’s synth warned about it.
Stack.addDependency→addStackDependency(deprecation).dev-isolation-check.shuseslist-stack-resources, becausedescribe-stack-resourcesreturns at most 100 resources and the stacks are now bigger.
- server/scripts/provision-tenant.sh, server/scripts/deprovision-tenant.sh: Platinum branches.
- server/cdk/lib/bootstrap-template/bootstrap-template-stack.ts: source bucket and output
SourceBucketName. - server/cdk/lib/bootstrap-template/application-plane-jobs.ts: ARM build image, CDK-role and delete-stack permissions,
SOURCE_BUCKETenv var. - server/cdk/bin/serverless-saas.ts:
CDK_PARAM_LAMBDA_RESERVE_CONCURRENCY. - scripts/deploy.sh, scripts/upload-source.sh
Deploy
Section titled “Deploy”./scripts/deploy.sh you@example.com # deploys the stacks, then uploads source.zipVerify
Section titled “Verify”ADMIN_PASSWORD='Passw0rd!Saas' ./scripts/onboard-tenant.sh globex globex-admin@example.com PLATINUMOn the test deployment the CodeBuild job took about 8 minutes (npm ci, synth, Docker bundling, CloudFormation). Result:
{ "tenantName": "globex", "tier": "PLATINUM", "tenantStatus": "Complete", "tenantId": "9cb2ba3a-04af-4d35-ba01-d321bceeecbe", "tenantConfig": "{\"userPoolId\":\"us-east-1_0WpUyMfNM\",\"appClientId\":\"68cp...\",\"apiGatewayUrl\":\"https://ocgv3r2do8.execute-api.us-east-1.amazonaws.com/prod/\"}" }The user pool and API are different from the pooled ones (us-east-1_a1M1hAn8y, 0xlsgzuyxh).
| Check | Result |
|---|---|
globex admin /whoami on the silo API |
tenantTier: PLATINUM, apiKeyId: nd2li421x1 (= apiKeyPlatinumTierKeyId) |
| globex creates and lists products | ["Globex Gizmo"], stored in the silo’s own table |
| globex token on the pooled API | 401 (different user pool, so the signature/issuer check fails) |
| alice (pooled) token on the silo API | 401 |
| silo usage plan | 300 rps, 10,000/day |
| silo Lambda | reserved concurrency 5, IS_POOLED_DEPLOY=false |
| mapping table | {tenantId: 9cb2..., stackName: serverless-saas-tenant-template-9cb2..., codeCommitId: 7bvb...(S3 version), waveNumber: 1} |
Right after the silo stack is created its routes can return
403 Forbiddenfor a minute or two (same propagation as before). Retry.
Offboard:
ADMIN_PASSWORD='Passw0rd!Saas' ./scripts/offboard-tenant.sh globexThis deletes the stack serverless-saas-tenant-template-<tenantId> and the mapping row, and sets the registration to Deleted.
Cost note
Section titled “Cost note”Each Platinum tenant is a full stack (API, ~17 Lambdas, 2 tables, user pool). Idle cost is close to zero because everything is serverless and on-demand, but reserved concurrency takes 5 × functions out of the account’s concurrency pool per tenant.
Step 10 starts the web apps: S3 + CloudFront hosting and the React admin console for system admins.