Configuration
Every CephalonEngine app reads configuration through the standard ASP.NET Core IConfiguration chain. The conventions documented here apply equally to:
appsettings.json/appsettings.<Environment>.json- Environment variables (using the
__separator) - Azure Key Vault, AWS Parameter Store, GCP Secret Manager
dotnet user-secrets(development)- Any custom
IConfigurationProvider
The Engine:* namespace is the only key prefix CephalonEngine reads. Everything outside it is yours.
Top-level shape
Section titled “Top-level shape”{ "Engine": { "Id": "acme-store", // required — unique identifier emitted in logs + manifest "Deployment": { "Id": "prod-eu-west", // optional — labels traces with deployment instance "Topology": "SingleHost" // SingleHost | Microservice | MicroserviceSuite }, "Composition": { "Model": "Modular" }, // Modular (only option currently) "FeatureOrganization": "ModuleFirst", // ModuleFirst | VerticalSlice "Transports": [ "RestApi" ], // RestApi | JsonRpc | Grpc | GraphQL | ServerSentEvents | WebSocket "Data": { "IdStrategy": "Sfid", // Sfid (default) | Guid | Long "Provider": null, // null | "EntityFramework" | "Direct" "ReadModel": { }, // optional — separate provider for read side "WriteModel": { } // optional — separate provider for write side }, "Identity": { "Enabled": false, // capability gate "Provider": null, // null | "Bearer" | "Cookie" | custom "Authority": null, // for Bearer: IdP authority URL "Audience": null // for Bearer: expected audience claim }, "Tenancy": { "Enabled": false, // capability gate "Resolvers": [] // ["subdomain", "header", "claim", "path"] }, "Audit": { "Enabled": true, // default-on capability "Provider": "EntityFramework" // null | "EntityFramework" | "InMemory" }, "Messaging": { "Enabled": false, // capability gate "Provider": null, // null | "Wolverine" "Wolverine": { /* provider-specific options */ } }, "Observability": { "Provider": "OpenTelemetry", // null | "OpenTelemetry" | "AzureMonitor" | "Serilog" | … "Endpoint": "http://localhost:4317", // OTLP collector endpoint "Headers": null, // OTLP headers (auth) "Sampling": { "Ratio": 1.0 } // 0.0 - 1.0 trace sampling } }, "ConnectionStrings": { "Default": "Host=…;Port=5432;Database=…;Username=…;Password=…" }, "Logging": { "LogLevel": { "Default": "Information", "Cephalon.Engine": "Information" } }}Required keys
Section titled “Required keys”| Key | Why required | What happens if missing |
|---|---|---|
Engine:Id | Emits in every log entry, manifest, and OTLP resource attribute. Without it operators can’t correlate signals across replicas. | Composition fails with EngineIdMissingException. |
Everything else has a safe default — see each section below.
Per-environment configuration
Section titled “Per-environment configuration”The standard ASP.NET Core override chain applies:
appsettings.json ↓ overridden byappsettings.<ASPNETCORE_ENVIRONMENT>.json ↓ overridden byUser secrets (Development only) ↓ overridden byEnvironment variables ↓ overridden byCommand-line argsExample pattern — connection strings:
{ "ConnectionStrings": { "Products": "" // placeholder; never commit a real connection string }}{ "ConnectionStrings": { "Products": "Host=localhost;Port=5432;Database=products_dev;Username=postgres;Password=postgres" }}ASPNETCORE_ENVIRONMENT=ProductionConnectionStrings__Products=Host=prod-db;Port=5432;Database=products;Username=svc;Password=<from-vault>Environment-variable mapping
Section titled “Environment-variable mapping”ASP.NET Core’s __ separator converts the nested JSON shape into env-var names. Pattern: Engine__<Section>__<Key>.
| Configuration key | Environment variable |
|---|---|
Engine:Id | Engine__Id |
Engine:Deployment:Topology | Engine__Deployment__Topology |
Engine:Data:Provider | Engine__Data__Provider |
Engine:Tenancy:Enabled | Engine__Tenancy__Enabled |
Engine:Messaging:Wolverine:Transport | Engine__Messaging__Wolverine__Transport |
ConnectionStrings:Default | ConnectionStrings__Default |
Logging:LogLevel:Default | Logging__LogLevel__Default |
Array values use indices:
Engine__Transports__0=RestApiEngine__Transports__1=Grpcexport instead of $env:. Docker Compose and Kubernetes ConfigMaps/Secrets work the same way. The __ separator is portable.Secret management
Section titled “Secret management”Connection strings, API keys, and IdP credentials should never be in appsettings.json. Use one of:
Local development — dotnet user-secrets
Section titled “Local development — dotnet user-secrets”cd src/Acme.Store.Hostdotnet user-secrets initdotnet user-secrets set "ConnectionStrings:Products" "Host=…"Secrets are stored at %APPDATA%\Microsoft\UserSecrets\<id>\secrets.json (Windows) or ~/.microsoft/usersecrets/<id>/secrets.json (Linux/macOS). Not in your repo.
Azure Key Vault
Section titled “Azure Key Vault”builder.Configuration.AddAzureKeyVault( new Uri($"https://{builder.Configuration["KeyVault:Name"]}.vault.azure.net/"), new DefaultAzureCredential());Key Vault secrets named Engine--Data--Provider map to Engine:Data:Provider (Key Vault uses -- because : isn’t allowed in names).
AWS Parameter Store
Section titled “AWS Parameter Store”Use the Amazon.Extensions.Configuration.SystemsManager package; parameter names map the same way (/Engine/Data/Provider).
Kubernetes Secrets
Section titled “Kubernetes Secrets”Mount as env vars in the pod spec:
env: - name: ConnectionStrings__Products valueFrom: secretKeyRef: name: acme-store-secrets key: products-connectionSection reference
Section titled “Section reference”The sub-pages below document each Engine:* section in full. The most-used keys for adopters:
| Section | Purpose | Required for |
|---|---|---|
| Engine:Id and deployment identity (covered inline above) | Engine ID, deployment ID, topology | Always |
| Engine:Data | Id strategy, provider selection, read/write split | Any data-using module |
| Engine:Identity | Bearer/Cookie auth, JWT validation, claim mapping, scope enforcement | Identity-aware apps |
| Engine:Tenancy | Resolvers (subdomain/header/claim/path), sharding, governance | Multi-tenant apps |
| Engine:Audit (coming soon) | Audit storage and policy | Audit-tracked apps |
| Engine:Messaging | Wolverine + transport (in-memory/RabbitMQ/Azure SB/SQS/Kafka), routing, DLQ | Event-driven apps |
| Engine:Transports (covered inline above) | Enabling transports per host (RestApi, Grpc, JsonRpc, …) | All HTTP/gRPC apps |
| Engine:Observability | OTel provider, endpoint, sampling, logs/metrics/traces, 14 cloud adapters | All production apps |
| Error handling (coming soon) | ProblemDetails shape, exception mapping, validation contract | All apps |
Common patterns
Section titled “Common patterns”Pattern: feature-flagged capability via configuration
Section titled “Pattern: feature-flagged capability via configuration”You want to roll out a new capability per environment without code changes.
{ "Engine": { "Messaging": { "Enabled": false } } }{ "Engine": { "Messaging": { "Enabled": true, "Provider": "Wolverine" } } }In code, the module declares the capability conditionally:
public override ModuleDescriptor Describe() => new( name: "Acme.Orders", version: "1.0.0", capabilities: messagingEnabled ? [Capability.Data, Capability.Eventing] : [Capability.Data]);Pattern: per-tenant data sharding via configuration
Section titled “Pattern: per-tenant data sharding via configuration”Some tenants live on a dedicated database. Configure via a connection-string map:
{ "Engine": { "Tenancy": { "Enabled": true, "Resolvers": ["subdomain"], "Sharding": { "default": "ConnectionStrings:DefaultShard", "acme": "ConnectionStrings:AcmeShard", "globex": "ConnectionStrings:GlobexShard" } } }}In the data module, resolve the connection based on the current tenant:
services.AddCephalonEntityFramework<AppDb>((sp, opts) =>{ var tenant = sp.GetRequiredService<ITenantContext>(); var key = sp.GetRequiredService<IConfiguration>() .GetSection($"Engine:Tenancy:Sharding:{tenant.Id}") .Value ?? "ConnectionStrings:DefaultShard"; var conn = sp.GetRequiredService<IConfiguration>()[key]; opts.UseNpgsql(conn);});Pattern: configuration validation at startup
Section titled “Pattern: configuration validation at startup”Use the standard IOptions<T> + OptionsValidator pattern:
public sealed class ProductsOptions{ [Required] public string ConnectionString { get; init; } = string.Empty;
[Range(1, 100)] public int PageSize { get; init; } = 25;}
services.AddOptions<ProductsOptions>() .Bind(configuration.GetSection("Engine:Products")) .ValidateDataAnnotations() .ValidateOnStart(); // fail composition if invalidPattern: hot-reload on config change
Section titled “Pattern: hot-reload on config change”services.Configure<ProductsOptions>(configuration.GetSection("Engine:Products"));// Inject IOptionsMonitor<ProductsOptions> — re-fires when appsettings.json changesEngine:Messaging:Enabled from false → true at runtime won’t register the Wolverine provider mid-process — composition is one-shot. Restart for capability changes; only “tunable” options should be hot-reloaded.Configuration validation in cephalon doctor
Section titled “Configuration validation in cephalon doctor”cephalon doctor --config ./appsettings.json (planned for the 0.2.0-preview release) will pre-validate the Engine:* schema before you run the host. Today, the same validation runs during composition — composition fails fast with a clear error.
Limits
Section titled “Limits”- Connection strings live under
ConnectionStrings:*, notEngine:Data:ConnectionString. This follows the ASP.NET Core convention; many libraries (EF Core, dependency-health probes) look atConnectionStrings:*automatically. - The
Engine:*schema is versioned with the engine. Adding new options is additive within a preview minor; renames or removals require a major bump. See Migration → Breaking changes. - Configuration is one-shot for capabilities. Flipping
Enabled: true → falseforIdentity,Tenancy,Messaging, etc. requires a host restart (the capability registry is built during composition). - Per-module configuration uses
Engine:<ModuleName>:*by convention so the runtime can introspect per-module settings. Modules that read from arbitraryIConfigurationpaths work but won’t show up in module-introspection tooling.
Tips & tricks
Section titled “Tips & tricks”Practical guidance for keeping Engine:* configuration sane across environments.
Layering & overrides
Section titled “Layering & overrides”- Use
appsettings.<Env>.jsonfor environment-specific defaults, not absolute values. Don’t repeat what’s already inappsettings.json. - Use environment variables for secrets + ops toggles only. Code-level defaults stay in JSON so a developer can read them.
- Never commit
appsettings.Production.jsonwith real values. It exists to document the shape and provide placeholders; production fills in via env vars / Key Vault.
Schema discipline
Section titled “Schema discipline”- Treat
Engine:*as read-only after composition. Anything tunable at runtime should live under its ownEngine:<Feature>:*subsection so it’s clear what’s safe to change. - Validate at startup, not on first request.
IOptions<T>with.ValidateOnStart()makes config errors crash the host visibly, instead of throwing 500s once traffic arrives. - Use
IOptionsMonitor<T>only for genuinely tunable values (timeouts, page sizes, feature flags). Boot-time options should beIOptions<T>so changes only take effect on restart — clearer mental model.
Secrets
Section titled “Secrets”- Never echo a connection string in logs, even at
Debuglevel. Use[Sensitive]attributes if you bind config to POCOs you ever serialise. - Treat env vars as plaintext. They live in
/proc/*/environand process listings. Use Key Vault / Parameter Store for the actual secret; env vars carry the reference, not the value. - Rotate connection strings as a deploy event, not a config-reload event. Hot-reloading a connection string mid-request can corrupt connection pools.
Naming conventions
Section titled “Naming conventions”| Thing | Convention | Example |
|---|---|---|
| Connection-string key | ConnectionStrings:<Module> (PascalCase) | ConnectionStrings:Products |
| Engine ID | <product>-<role> (kebab, no env) | acme-store |
| Deployment ID | <env>-<region> (kebab) | prod-eu-west-1 |
| Custom config section | Engine:<ModuleName>:* | Engine:Products:CatalogTtl |
| OTLP endpoint | full URL incl. scheme | https://otlp.example/v1/traces |
Common pitfalls
Section titled “Common pitfalls”- Env var case matters on Linux.
Engine__Idis not the same asengine__id. Match the JSON path exactly. __separator only, not:in env vars.:isn’t a legal env-var character on many shells.- Arrays via env vars need indices.
Engine__Transports__0=RestApithenEngine__Transports__1=Grpc— notEngine__Transports=RestApi,Grpc. - Empty string ≠ unset.
Engine__Identity__Authority=(empty) is treated as “value is empty string”, not “missing”. Configure validators accordingly.
Per-environment patterns
Section titled “Per-environment patterns”appsettings.json ← shape + safe defaults (committed)appsettings.Development.json ← dev-loop convenience (committed)appsettings.<Env>.json ← env-specific config (committed, no secrets)secrets via env vars / Key Vault ← per-deployDebugging config
Section titled “Debugging config”/engine/snapshotreturns the resolved configuration as the engine sees it. Useful for “is my env var being read?”.dotnet user-secrets listin dev shows what your local user-secrets actually contain.- In Azure App Service / Container Apps, the portal shows env vars as Application Settings. Match them against
/engine/snapshotto verify wiring.
Anti-patterns
Section titled “Anti-patterns”| Don’t | Do |
|---|---|
Commit appsettings.Production.json with real connection strings | Use env vars or Key Vault references; appsettings.Production.json has placeholders |
Read IConfiguration directly inside handlers | Bind to a strongly-typed POCO and inject IOptions<T> |
Have Engine:* keys outside the documented schema | Use a custom namespace (Acme:* or Engine:<ModuleName>:*); engine-blessed keys stay typed |
| Toggle capabilities at runtime via hot-reload | Restart the host — capability registration is one-shot |
Treat appsettings.json as the source of truth in CI tests | Pass overrides via env vars in CI so tests reflect production wiring |
Where to go next
Section titled “Where to go next”Drill into a specific Engine:* section
Section titled “Drill into a specific Engine:* section”- Engine:Data — every
Id,Provider,Outbox,Inbox,Migrations, andSfidoption, with 4 end-to-end scenarios. - Engine:Identity — Bearer + Cookie schemes, JWT validation, claim mapping, scope policy.
- Engine:Tenancy — resolver pipeline, sharding strategy, per-tenant data isolation.
- Engine:Messaging — Wolverine transports, routing, retries, dead-letter, scheduled delivery.
- Engine:Observability — OTel collector targets, sampling, logs/metrics/traces, 14 cloud-vendor presets.
Related reading
Section titled “Related reading”- Concepts — the model behind the config schema.
- Reference → Runtime contracts — the runtime side of the contract.
- Reference → CLI —
cephalon doctorandcephalon newflags. - Operations → Production readiness — the configuration checks to verify before going live.