Configuring a Tenant
Tenant files isolate each customer's database, Redis cluster, Kafka brokers, and feature flags without touching the base configuration. A full worked example with Apex Retail Group.
How It Works
When TENANT_CODE is set, two additional files are loaded after all base and local files:
appsettings.Tenant.{code}.json ← tenant defaults (all environments)
appsettings.Tenant.{code}.{env}.json ← tenant + environment specific (last wins)
If TENANT_CODE is empty, both files are skipped and the app runs on base defaults.
The Split Rule
What belongs in Tenant.{code}.json | What belongs in Tenant.{code}.{env}.json |
|---|---|
| CORS allowed origins | Database connection strings |
| Kafka topic names and partition counts | Kafka broker addresses and credentials |
| Cache type (Redis vs Memory) | Redis connection string and password |
| LLM model selection | LLM API key |
| Feature flags | JWT signing key |
| Log level overrides | Observability endpoints and API keys |
| SSO provider choice | SSO client secret |
Tenant.{code}.json) should contain nothing that would change between a dev
and production deployment. Anything infrastructure-specific goes in the env-specific file.
Worked Example — Apex Retail Group
Apex Retail Group is a multi-location retail chain using BizFirstAI for their operations platform. They have dedicated infrastructure: a SQL Server cluster, a Redis cache for cart and session data, and a three-node Kafka cluster for real-time inventory and order events across stores.
Tenant code: apexretail
Set TENANT_CODE
In launchSettings.json for development:
"TENANT_CODE": "apexretail"
On production servers: system environment variable or container env var.
appsettings.Tenant.apexretail.json committed
No secrets. No addresses. Only stable tenant identity and preferences.
{
"CorsSettings": {
"AllowedOrigins": [
"https://portal.apexretail.com",
"https://admin.apexretail.com",
"https://pos.apexretail.com"
]
},
"SharpCache": {
"CacheType": "Redis"
},
"EdgeStream": {
"Kafka": {
"Enabled": true,
"EventTopicName": "apexretail.events",
"DeadLetterTopicName": "apexretail.events.dlq",
"ConsumerGroupId": "apexretail-signalr",
"ChatTopicName": "apexretail.chat",
"SeparateChatTopic": true,
"TopicPartitionCount": 12,
"TopicReplicationFactor": 3,
"Acks": "all",
"EnableIdempotence": true,
"CompressionType": "snappy"
}
},
"Agent": {
"LlmConfig": {
"Provider": "openai",
"Model": "gpt-4o"
}
},
"Account": {
"NewUserVerification": true
},
"FlowSecurity": {
"Security": {
"ScopeBoundaryEnabled": true,
"AllowPublicWorkflows": false
}
}
}
appsettings.Tenant.apexretail.production.json deployed — not committed
Real infrastructure addresses and secrets. Deployed by CI/CD from a vault. Never in the repository.
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST;Integrated Security=False;User ID=bizfirst_svc;Password=<vault>;Encrypt=True;TrustServerCertificate=False;Connect Timeout=29"
},
"BizFirstAIDatabase": {
"Default": "Data Source=sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST;...",
"DefaultConnection": {
"Master": "Data Source=sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST;..."
},
"BizFirstAI": {
"Master": "Data Source=sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST;..."
},
"Redis": "redis.apexretail.local:6379,password=<vault>,ssl=false,abortConnect=false"
},
"Database": {
"BizFirstAI": {
"Master": "Data Source=sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST;..."
},
"Redis": "redis.apexretail.local:6379,password=<vault>,ssl=false,abortConnect=false"
},
"SqlServerAtlasStorage": {
"ConnectionString": "Data Source=sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST;...",
"EnableDetailedLogging": false,
"EnableMetrics": true
},
"EdgeStream": {
"Kafka": {
"BootstrapServers": "kafka1.apexretail.local:9092,kafka2.apexretail.local:9092,kafka3.apexretail.local:9092",
"SecurityProtocol": "sasl_ssl",
"SaslMechanism": "PLAIN",
"SaslUsername": "bizfirst-svc",
"SaslPassword": "<vault>",
"SslCaLocation": "/etc/ssl/certs/apexretail-ca.crt"
}
},
"BizFirst": {
"Observability": {
"ServiceName": "BizFirst.Ai.ApexRetail",
"Environment": "production",
"Tracing": {
"Enabled": true,
"OtlpEndpoint": "http://otel.apexretail.local:4317",
"SamplingRate": 0.1
},
"Logging": {
"Loki": {
"Enabled": true,
"Url": "http://loki.apexretail.local:3100",
"MinimumLevel": "Warning"
}
},
"Grafana": {
"Enabled": true,
"Url": "https://grafana.apexretail.local",
"ApiKey": "<vault>"
}
}
},
"JWT": {
"Key": "<32-byte-base64-from-vault>",
"Issuer": "BizFirst.ApexRetail",
"Audience": "ApexRetail.Clients",
"ExpiryMinutes": 480
},
"LlmProviders": [
{
"Provider": "openai",
"Models": [
{
"Id": "gpt-4",
"Name": "gpt-4o",
"ApiKey": "<vault>",
"Type": "chat",
"MultiModal": true
}
]
}
]
}
appsettings.Tenant.apexretail.development.json committed
Points developers at a shared dev database and local infrastructure. No secrets.
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=dev-sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST_DEV;Integrated Security=True;Encrypt=Optional;TrustServerCertificate=True"
},
"BizFirstAIDatabase": {
"Default": "Data Source=dev-sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST_DEV;Integrated Security=True;Encrypt=True;TrustServerCertificate=True;Command Timeout=0",
"DefaultConnection": {
"Master": "Data Source=dev-sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST_DEV;Integrated Security=True;Encrypt=True;TrustServerCertificate=True;Command Timeout=0"
},
"BizFirstAI": {
"Master": "Data Source=dev-sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST_DEV;Integrated Security=True;Encrypt=True;TrustServerCertificate=True;Command Timeout=0"
},
"Redis": "localhost:6379,abortConnect=false"
},
"Database": {
"BizFirstAI": {
"Master": "Data Source=dev-sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST_DEV;Integrated Security=True;Encrypt=True;TrustServerCertificate=True;Command Timeout=0"
},
"Redis": "localhost:6379,abortConnect=false"
},
"SqlServerAtlasStorage": {
"ConnectionString": "Data Source=dev-sql.apexretail.local;Initial Catalog=APEXRETAIL_BIZFIRST_DEV;Integrated Security=True;Encrypt=True;TrustServerCertificate=True;Command Timeout=0",
"EnableDetailedLogging": true
},
"EdgeStream": {
"Kafka": {
"BootstrapServers": "localhost:9092",
"SecurityProtocol": "plaintext"
}
},
"BizFirst": {
"Observability": {
"ServiceName": "BizFirst.Ai.ApexRetail.Dev",
"Environment": "development",
"Tracing": { "SamplingRate": 1.0 },
"Logging": {
"Loki": { "Enabled": false }
}
}
}
}
Apex Retail File Summary
| File | Committed | Purpose |
|---|---|---|
appsettings.Tenant.apexretail.json | Yes | CORS, Kafka topics, cache type, LLM model, feature flags |
appsettings.Tenant.apexretail.production.json | No — deployed | Real DB, Redis cluster, Kafka brokers, JWT key, API keys |
appsettings.Tenant.apexretail.development.json | Yes | Dev DB, local Redis/Kafka, verbose logging |
appsettings.Tenant.apexretail.staging.json | No — deployed | Staging infrastructure |
Adding a New Tenant — Checklist
Choose a tenant code
Short, lowercase, no spaces: acmecorp, sunrisehr, apexretail.
Set TENANT_CODE
In launchSettings.json for local dev; system env var or container env for servers.
Create the base tenant file
appsettings.Tenant.{code}.json — CORS, topics, flags. No secrets.
Create the dev env file
appsettings.Tenant.{code}.development.json — shared dev DB. Committed.
Create and deploy the prod env file
appsettings.Tenant.{code}.production.json — real infra + secrets. From vault. Never committed.