Portal Community

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}.jsonWhat belongs in Tenant.{code}.{env}.json
CORS allowed originsDatabase connection strings
Kafka topic names and partition countsKafka broker addresses and credentials
Cache type (Redis vs Memory)Redis connection string and password
LLM model selectionLLM API key
Feature flagsJWT signing key
Log level overridesObservability endpoints and API keys
SSO provider choiceSSO client secret
Design principle The base tenant file (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

FileCommittedPurpose
appsettings.Tenant.apexretail.jsonYesCORS, Kafka topics, cache type, LLM model, feature flags
appsettings.Tenant.apexretail.production.jsonNo — deployedReal DB, Redis cluster, Kafka brokers, JWT key, API keys
appsettings.Tenant.apexretail.development.jsonYesDev DB, local Redis/Kafka, verbose logging
appsettings.Tenant.apexretail.staging.jsonNo — deployedStaging infrastructure

Adding a New Tenant — Checklist

1

Choose a tenant code

Short, lowercase, no spaces: acmecorp, sunrisehr, apexretail.

2

Set TENANT_CODE

In launchSettings.json for local dev; system env var or container env for servers.

3

Create the base tenant file

appsettings.Tenant.{code}.json — CORS, topics, flags. No secrets.

4

Create the dev env file

appsettings.Tenant.{code}.development.json — shared dev DB. Committed.

5

Create and deploy the prod env file

appsettings.Tenant.{code}.production.json — real infra + secrets. From vault. Never committed.