Skip to content

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.

One tenant template deployed as the pooled stack and as one silo stack per Platinum tenant

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.

  1. scripts/deploy.sh now ends by running scripts/upload-source.sh, which zips server/ (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 (codeCommitId in the mapping table, used by the pipeline in step 12).
  2. On a PLATINUM onboarding event, provision-tenant.sh downloads the newest source.zip, runs npm ci and then:
    Terminal window
    CDK_PARAM_TENANT_ID=$tenantId npx cdk deploy serverless-saas-tenant-template-$tenantId --exclusively
    --exclusively deploys only that stack, and the CDK CLI only bundles that stack’s assets.
  3. 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 deploy assumes the CDK bootstrap roles (cdk-*-deploy-role, cdk-*-file-publishing-role), and CloudFormation itself runs as cdk-*-cfn-exec-role. The job only needs sts:AssumeRole on cdk-* 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).

  • 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.sh uses list-stack-resources, because describe-stack-resources returns at most 100 resources and the stacks are now bigger.
Terminal window
./scripts/deploy.sh you@example.com # deploys the stacks, then uploads source.zip
Terminal window
ADMIN_PASSWORD='Passw0rd!Saas' ./scripts/onboard-tenant.sh globex globex-admin@example.com PLATINUM

On 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 Forbidden for a minute or two (same propagation as before). Retry.

Offboard:

Terminal window
ADMIN_PASSWORD='Passw0rd!Saas' ./scripts/offboard-tenant.sh globex

This deletes the stack serverless-saas-tenant-template-<tenantId> and the mapping row, and sets the registration to Deleted.

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.