agentsclimarketplace

Scaffolding multi tenant azure apps

Skill alexpizarro/azure-lean-stack-skills/skills/scaffolding-multi-tenant-azure-apps

Scaffolds a multi-tenant Azure app where each tenant gets its own resource group on a shared subscription, with one Bicep main.bicep driven by a single tenant parameter that cascades through every resource name. The default tenant keeps the original (single-tenant) names byte-identical so the pattern works for the first deployment with no migration. Use when onboarding the second tenant onto an app that started single-tenant, designing a multi-school/multi-clinic/multi-org SaaS, or refactoring shared resources to be per-tenant.From its SKILL.md

Install
npx -y skills add alexpizarro/azure-lean-stack-skills --skill scaffolding-multi-tenant-azure-apps

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

3 things to look at

  • reads credentialsReads from 3 credential sources: `sqlAdminPassword` and 2 more.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 3 commands, including `az resource list --tag tenant=$SLUG -o table` and 2 more.

SKILL.md

6.4 KB, ~1.5k tokens by cl100k_base, as published. Nobody here has run it

Scaffolding Multi-Tenant Azure Apps

Multi-tenant pattern with one resource group per tenant on a shared subscription. One tenant parameter drives the RG and every resource name. Adding a tenant = redeploy with a new param value.

When to use this pattern

NeedThis skill
One product, multiple isolated customers (schools, clinics, agencies)Yes
Per-tenant DB, per-tenant storage, per-tenant ACAYes
Multi-tenant SaaS where customers share a single DBNo — that's row-level multi-tenancy, not infra-level
Different products for different customersNo — use per-product subscriptions instead

Workflow checklist

Copy this checklist and tick items off:

Onboarding a new tenant:
- [ ] Step 1: Agree on the tenant slug (kebab-case, ≤20 chars, no Azure reserved words)
- [ ] Step 2: Verify the derived storage account name fits (≤24 chars, alphanumeric) — provide override if not
- [ ] Step 3: Copy infra/environments/prod.parameters.json → infra/environments/{tenant}.parameters.json
- [ ] Step 4: Edit tenant slug + storageAccountName override if needed
- [ ] Step 5: Create a new branch for this tenant if using branch-per-tenant (recommended for full isolation)
- [ ] Step 6: Add OIDC SP + federated credential bound to the new branch (skill: configuring-azure-oidc-for-github-actions)
- [ ] Step 7: Generate per-tenant SQL password + add as GitHub secret
- [ ] Step 8: Push to the tenant branch → workflow provisions the isolated stack
- [ ] Step 9: Verify per-tenant cost reporting: `az resource list --tag tenant=$SLUG -o table`

The core idea

main.bicep accepts a tenant param. Default value matches your original (single-tenant) naming so the first deployment doesn't need to migrate anything:

@description('Tenant slug — drives all resource names + the RG.')
param tenant string = 'acme-default'

// Storage account name can\'t have hyphens, ≤24 chars. Provide an override
// for tenants whose slug doesn\'t yield a valid storage name.
@description('Storage account name override (no hyphens, ≤24 chars). Empty = derive from tenant.')
param storageAccountName string = ''

module rg 'modules/resourceGroup.bicep' = {
  name: 'deploy-rg-${environment}'
  params: { name: '${tenant}-rg-${environment}' }
}

module storage 'modules/storage.bicep' = {
  scope: resourceGroup('${tenant}-rg-${environment}')
  params: {
    name: empty(storageAccountName) ? toLower(replace('${tenant}store', '-', '')) : storageAccountName
  }
}

// ... every other module follows the same pattern

See references/tenant-isolation.md for the trade-offs and references/one-rg-per-tenant.md for the why.

Adding a new tenant

# 1. Copy the parameter file
cp infra/environments/prod.parameters.json infra/environments/acme-corp.parameters.json

# 2. Edit the new file — set tenant slug + storage override if needed
{
  "parameters": {
    "environmentName": { "value": "prod" },
    "tenant":          { "value": "acme-corp" },
    "storageAccountName": { "value": "acmecorpstoreprod" }
  }
}

# 3. Deploy
az deployment sub create \
  --location australiaeast \
  --template-file infra/main.bicep \
  --parameters @infra/environments/acme-corp.parameters.json \
  --parameters sqlAdminPassword="$NEW_PASSWORD"

The deploy creates a fully isolated stack: new RG, new SQL, new Storage, new SWA, new ACA — same code paths, separate resources.

Per-tenant naming

ResourceName
Resource Group{tenant}-rg-{env}
Static Web App{tenant}-swa-{env}
SQL Server{tenant}-sql-{env}
SQL Database{tenant}-db-{env}
Storage account{tenant}store{env} (no hyphens, ≤24 chars)
Container App{tenant}-aca-{env}
App Insights{tenant}-ai-{env}

The org + project formula becomes a tenant formula. If you already shipped single-tenant with {org}-{project}-* names, set tenant = '{org}-{project}' as the default — the names stay byte-identical.

Per-tenant secrets

GitHub secrets are per-repo, not per-tenant. Two options:

  1. One repo per tenant — heavy, but each tenant gets its own SP, secrets, deployment pipeline. Best for fully separate compliance domains.
  2. One repo, environment-scoped secrets — use GitHub Environments named after the tenant. Each environment has its own SQL_ADMIN_PASSWORD, AZURE_CLIENT_ID, etc. Best for ~5–20 tenants.

The federated credential subject changes to:

repo:{owner}/{repo}:environment:acme-corp-prod

Per-tenant cost tracking

Tag every resource with the tenant slug. Azure Cost Management can then group by tag:

var tags = {
  tenant: tenant
  environment: environment
  managedBy: 'bicep'
}

Then in Cost Management:

Group by: Tag: tenant

Per-tenant invoices/chargebacks become trivial.

When to break this pattern

  • Shared resources by design — e.g. one ACR shared across all tenants for pulling the same image. Declare those in a separate infra/shared/main.bicep deployed once per region; refer to them by resource ID in tenant deployments.
  • Tenant-specific overrides — sometimes a customer needs a different SKU. Add a tenant-scoped param file and override.

Composes with

Templates

FilePurpose
templates/multi-tenant-main.bicepmain.bicep with the tenant param pattern

What ships with it: 3 files

12.7 KB alongside SKILL.md

templates/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.