Skip to content

Configuration reference (appsettings)

Everything a caller project can configure, section by section, with defaults and valid values. Genie is config-first: LoadFromConfiguration(configuration) on the GenieBuilder binds every engine section from the host’s IConfiguration as the base, and any fluent Use* / Configure* call afterwards overrides it in code (later wins). The identity, Hangfire, Data Protection, Assistant, and OpenAPI modules read IConfiguration directly (composed by AddGenieApp, or by your own AddGenieAuth / AddGenieHangfire / AddGenieAssistant calls).

Every key is also settable through standard ASP.NET Core environment variables — replace : with __ and index arrays: Genie__Cors__AllowedOrigins__0, Genie__Spa__Enabled=true, Genie__Assistant__ApiKey=sk-… (the right place for secrets).

A complete production-shaped appsettings.json

Section titled “A complete production-shaped appsettings.json”

Only set what you need — every value below that equals the default can be omitted. Secrets (passwords, API keys) belong in environment variables or a secret store, not in the file.

{
// --- Not Genie's: platform + library conventions, always at the root ---
"ConnectionStrings": {
"SqlServer": "Server=…;Database=…;…", // used when Genie:Datasource = SqlServer
"PostgreSql": "Host=…;Database=…;…", // used when Genie:Datasource = PostgreSql
"Redis": "localhost:6379,defaultDatabase=1" // cache, Hangfire, DataProtection, JWT keys
},
"Serilog": { /* … */ },
"AllowedHosts": "*",
// --- Everything Genie reads ---
"Genie": {
"Datasource": "SqlServer", // "SqlServer" | "PostgreSql"
"Errors": { "ExposeErrorDetails": false, "IncludeStackTrace": false },
"Spa": { "Enabled": true, "RootPath": "ui" },
"SchemaCache": { "Enabled": true, "TtlMinutes": 30 },
"Uploads": { "MaxImportBytes": 1073741824 },
"Warehousing": { "Enabled": false, "Targets": {} }, // replication to ClickHouse / Meilisearch
"Performance": { "Enabled": false },
"Warmup": { "Enabled": false }, // true = warm EF + view cache at startup
"Migration": {
"MigrationExecution": "Yes", // "No" | "Yes" (hash-checked) | "Forced"
"ModelsPath": "models" // absolute, or relative to the app / content root
},
"Cors": { "AllowedOrigins": [] }, // empty = no cross-origin callers (safe default)
"Storage": { "Type": "Local", "Path": "App_Data/storage" },
"Notifications": {
"Email": { "Enabled": true, "SmtpHost": "smtp.example.com", "SmtpPort": 587,
"FromEmail": "noreply@example.com", "Username": "noreply@example.com" },
"Sms": { "Enabled": false }
},
"Auth": {
"Jwt": { "Issuer": "MyApp", "Audience": "MyAppClient", "AccessTokenExpiryMinutes": 60 },
"Security": { "MaxInvalidLoginAttempts": 5, "SessionTimeoutMinutes": 480 },
"Mfa": { "Email": { "Enabled": true, "UseProductionCodes": true } },
"Features": { "RefreshTokens": true, "PasswordReset": true, "Impersonation": false },
"PasswordEncryption": {
"SeedAdminPassword": "…", // first-boot system password — CHANGE IT
"MasterKeyPassword": "…" // required for DB password encryption
}
},
"Hangfire": { "RedisPrefix": "myapp-jobs" },
"DataProtection": { "Enabled": true, "Provider": "Redis", "ApplicationName": "MyApp" },
"Diagnostics": { "AllowedIPs": [ "10.0.0.5" ] }
}
}

Every engine-owned section was consolidated under Genie:. The legacy paths are no longer read — on startup LoadFromConfiguration inspects your configuration and throws, listing each stale path next to its replacement, so a half-configured app can never boot silently.

Legacy path Now
AppSettings:Datasource Genie:Datasource
Startup Genie:Migration — and ExecuteMigration → MigrationExecution
Storage Genie:Storage
Notifications:Email / :Sms / :Vapid Genie:Notifications:Email / :Sms / :Vapid
Assistant Genie:Assistant
Security:Cors Genie:Cors
Security:SeedAdminPassword / MasterKeyPassword / PasswordEncryptionKey Genie:Auth:PasswordEncryption:*
Security:Login / Security:Recaptcha / Security:PasswordReset Genie:Auth:Login / :Recaptcha / :PasswordReset
Security (lockout, password policy, sessions) Genie:Auth:Security
Authentication:MFA Genie:Auth:Mfa
Authentication:JwtSettings Genie:Auth:Jwt (incl. KeyPath)
Authentication:CookieName Genie:Auth:Cookies:CookieName
Authorization Genie:Auth:Authorization
PasswordEncryption Genie:Auth:PasswordEncryption
DataProtection Genie:DataProtection
ForwardedHeaders Genie:ForwardedHeaders
RateLimiting:Auth / :Otp Genie:RateLimiting:Auth / :Otp
DiagnosticsAccess:AllowedIPs Genie:Diagnostics:AllowedIPs
OpenApi:* Genie:OpenApi:*

Matching is on the exact section that moved, never a parent — so a host’s own Security:PublicForms or RateLimiting:PublicForms is left alone while Security:Cors and RateLimiting:Otp are still caught. A legacy section is only flagged when it carries a real value: empty leftovers (a "Section": {} stub, or an environment variable blanked rather than unset) express no intent and are ignored.

Value Meaning
SqlServer (default) SQL Server dialect — EF provider, hardcoded system views, engine SQL.
PostgreSql PostgreSQL dialect throughout.

Parsed case-insensitively; an unknown value throws at startup. Everything that touches SQL is dual-dialect — the datasource selects which variant runs.

Name Used for
SqlServer The application database when Genie:Datasource=SqlServer.
PostgreSql The application database when Genie:Datasource=PostgreSql.
Redis The distributed ICacheStore (refresh tokens, MFA gates, JWT keypair with JwtKeyStorage.CacheStore), Hangfire storage, and the Data-Protection key ring (Provider: "Redis"). Optional for single-node hosts that register an in-memory ICacheStore.

Engine sections (bound by LoadFromConfiguration)

Section titled “Engine sections (bound by LoadFromConfiguration)”

Genie:ReportingSources — named read-only datasources

Section titled “Genie:ReportingSources — named read-only datasources”

Named, host-owned databases that a report page dataset points at with Source="Name", that an object points at with <Table DataSource="Name">, and that the assistant’s source picker lists. The host owns which engine and which credentials each name resolves to, so a *.report.xml or *.view.xml carries no environment detail and the same definition imports into staging and production unchanged.

"Genie": {
"ReportingSources": [
{ "Source": "Default", "Dialect": "SqlServer", "ConnectionString": "SqlServerReadOnly", "IsDefault": true },
{ "Source": "Analytics", "Dialect": "ClickHouse", "ConnectionString": "Analytics", "TenantColumn": "CompanyId" },
{ "Source": "Warehouse", "Dialect": "PostgreSql", "ConnectionString": "Host=wh;Database=ops;Username=ro;Password=…" }
]
}

An array, not a map: the order here is the order the assistant’s source picker lists, so the host decides what a user reaches first, and each entry names itself.

Key Meaning
Source The name a dataset, an object or the picker resolves against. Required, and unique.
Dialect SqlServer, PostgreSql or ClickHouse. All three execute; the ClickHouse driver ships with the engine.
ConnectionString Either a ConnectionStrings key (first two entries above) or a literal connection string (third). Looked up as a name first; a miss means the value is the connection string.
TenantColumn Column carrying the tenant id. The assistant filters non-System callers on it.
SingleTenant true declares the source holds one company’s data, so no tenant filter is needed.
Enabled Default true. false takes the source out of every path at once — absent from the picker and refused by resolution — so a retired database fails loudly instead of being quietly read.
IsDefault Marks the source a new assistant chat starts on.

Default is reserved for the application’s own database. Declaring it points reports and the assistant at a read-only login on that same server — which is where a report’s read-only guarantee actually comes from; the SELECT-only parse check is defence in depth. A report dataset with no Source falls through to it, and a startup warning names the reports that moved. Objects deliberately do not fall through: they write, and this connection is read-only.

Names are matched case-insensitively. An unknown source, a disabled one, or one whose ConnectionString resolves to nothing all throw — naming the source and listing what is configured. Resolution deliberately does not fall back to the app’s own database, which would read the wrong data and look like it worked.

A connection string can be supplied out-of-band by the entry’s index, which is how a deployment keeps credentials out of the file — Genie__ReportingSources__1__ConnectionString replaces that one field on the second entry and leaves the rest of it alone. Positional, so reordering the array moves which source an override applies to.

In code: genie.UseSource("Analytics", source => { … }) declares or adjusts an entry, and configures rather than replaces — so setting one property in code does not drop a TenantColumn bound from configuration, which would silently refuse every non-System caller.

An object that names a source is read-only and builds its grid SQL — paging, sorting, filters, quick-search — in that source’s dialect. Its editor-field and column lookups still resolve against the application database. See Views → DataSource.

All three dialects execute. A ClickHouse source is the natural read side of warehousing: give the read source and the warehousing target the same name, and “analytics” means the same server whether something is writing to it or reading from it. See Reports → runtime.

Key Default Meaning
ExposeErrorDetails true Whether unexpected 5xx responses carry the real exception message. Expected 4xx errors always carry theirs. Set false in production.
IncludeStackTrace false Also include the exception type + stack trace in the envelope. Development only.

Fluent override: ConfigureErrors(e => …). Details: Errors.

Key Default Meaning
Enabled false Serve the built React UI from the API origin (zero CORS preflights).
RootPath (wwwroot) UI build output, relative to the content root — "../ui/dist" in dev, "ui" in a published app.
IndexFile "index.html" SPA entry document for fallback routes.
ApiPathPrefixes /api, /hubs, /files, /hangfire, /swagger, /openapi Never fall back to the SPA — JSON 404 instead.
ImmutablePathPrefixes /assets Content-hashed bundles ⇒ Cache-Control: … immutable.
ImmutableMaxAgeSeconds 31536000 Cache lifetime for immutable assets (1 year).

Fluent override: ConfigureSpa(s => …). Full walkthrough: Deployment.

Key Default Meaning
AllowedOrigins [] Origins allowed cross-origin. Empty = none (safe default; same-origin unaffected).
AllowedMethods [] → GET, POST, PUT, PATCH, DELETE, OPTIONS Empty inherits the default set.
AllowedHeaders [] → Authorization, Content-Type, X-Requested-With, X-Correlation-Id, X-TimeZone, X-XSRF-TOKEN, X-SignalR-User-Agent Empty inherits the default set (everything the Genie client sends, including the SignalR negotiate header).
ExposedHeaders [] Response headers readable by cross-origin script.
AllowCredentials false Required for the Genie client cross-origin; only honoured with explicit origins.
PreflightMaxAgeSeconds 7200 Preflight cache (Access-Control-Max-Age); 7200 is Chromium’s cap.

Fluent override: ConfigureCors(c => …); applied with app.UseGenieCors(). Details: Deployment → Option 3.

Genie:SchemaCache — view-schema process cache

Section titled “Genie:SchemaCache — view-schema process cache”
Key Default Meaning
Enabled true Cache resolved view schemas (cache-aside over ICacheStore). false = every resolve hits the ViewStore.
TtlMinutes 30 Absolute entry TTL; schema writes (model migration, execute-script) invalidate sooner.
Key Default Meaning
MaxImportBytes 1073741824 (1 GiB) Caps the request body and multipart form length for import-data uploads.

Genie:Idempotency — safe-retry for object mutations

Section titled “Genie:Idempotency — safe-retry for object mutations”
Key Default Meaning
Enabled true When true, object mutations honour an Idempotency-Key request header (a repeat replays the first response instead of re-running the write). Set false to ignore the header.
Ttl 24:00:00 (24 h) How long a key’s response is remembered (and how long an in-flight lock survives a crash).
HeaderName Idempotency-Key The request header carrying the client-generated key.

Keys are cached via ICacheStore (Redis when configured, else in-memory) and scoped per user + company. See Idempotency.

Key Default Meaning
Enabled false When true, each request’s step timings are collected and written as one consolidated log event per request. When false, nothing is collected or logged (a true no-op).

When enabled, the UseGeniePerformanceLogging() middleware times the whole request and emits a single log event per request. The message is a compact one-liner — [Perf] {Method} {Route} {Action} (HTTP method, request path, and the Controller.Action name) — and everything else is attached as structured ForContext properties so it doesn’t clutter the message template:

  • CorrelationId — the same Activity.Current?.Id ?? TraceIdentifier the error envelope uses.
  • StatusCode, TotalMs.
  • Steps — the full timing tree, rendered as an indented outline (under a synthetic root of the action + total time). Nesting reflects how steps were opened inside one another (a phase and the SQL it triggered):
ObjectController.FieldDataSet (took 51.4 ms)
⌊ FieldDataSet:ResolveView (took 2 ms)
⌊ FieldDataSet:Query (took 48 ms)
⌊ SQL: SELECT ... (took 47 ms)
⌊ Unattributed (binding/serialization/pipeline) (took 1.4 ms)

Two layers of steps are captured automatically, no code changes needed:

  • Every SQL statement through the engine’s data-access seam (shown as SQL: …).
  • Semantic phases across the object pipeline — grid (Grid:ResolveView, Grid:Permissions, Grid:BuildSql, Grid:Project, Grid:LinqQuery), forms (Form:ResolveView, Form:MapFields, Form:LoadExisting, Form:Validate, Form:PreProcess, Form:Save, Form:Workflow, …), metadata (Metadata:ResolveView, Metadata:Schema), field datasets (FieldDataSet:*), import (Import:Parse/Cook/Attachments/Save/Posting/Commit), export (Export:Query/Write), and uploads (Upload:Store).

The tree always ends with an Unattributed line whenever the tracked steps don’t add up to the total (above a ~1 ms threshold): it’s the request time spent outside any tracked step — model binding, response serialization, and the rest of the MVC/middleware pipeline — so you never have to subtract by hand to find the gap. A large Unattributed band on a data-heavy endpoint usually means response serialization; a large one only on the first request of a fresh process is cold-start (see Genie:Warmup below).

The event is written under the Genie.Performance Serilog source context, so the host can route it to its own sink (e.g. a dedicated file) with a Serilog filter. The correlation id is also pushed into the Serilog LogContext, so any ordinary log written during the request carries the same CorrelationId (requires .Enrich.FromLogContext(), which the sample host enables).

Add the middleware to your pipeline once, right after UseGenieExceptionHandler() — it’s safe to leave in unconditionally since it no-ops while disabled:

app.UseGenieExceptionHandler();
app.UseGeniePerformanceLogging(); // Genie.Engine.Hosting.Diagnostics

To time your own steps on top of the built-in ones, either inject IPerformanceTracker or use the ambient Performance.Current (both resolve to the same per-request tree, and steps opened inside another step nest under it; both are a no-op when the flag is off):

// Injected — for your own services:
public MyService(IPerformanceTracker perf) { … }
using (perf.Track("Conv:Query")) { /* … the step to measure */ }
// Ambient — for static / DI-less code (Genie.Engine.Core.Abstractions):
using (Performance.Current?.Track("Conv:Query")) { /* … */ }
// Already measured a phase with your own Stopwatch? Surface it as a leaf step:
perf.Record("Conv:Query", elapsedMs);
Key Default Meaning
Enabled false When true, a one-shot background task runs once at startup (after model migrations settle) to remove the first-request cold-start cost. When false, nothing runs (a true no-op).
PreloadViews true Also pre-resolve every ViewStore view so the schema cache is warm for the first request. false still forces the first EF query (model/plan compile + connection-pool prime) but leaves the view cache cold.

The first request against a freshly started process pays a one-time cost the steady state doesn’t: JIT compilation, the EF Core model + query-plan build, and opening the first database connection. In a performance trace this shows up as the initial metadata/table hit being an order of magnitude slower than an identical request a second later (visible as inflated *:ResolveView/SQL/Unattributed timings that collapse on the next call). Enabling warm-up runs that cost once at startup, off the request path, so the first real user request is already warm:

"Genie": { "Warmup": { "Enabled": true } }

It waits for model migrations to be ready (or disabled) before touching the ViewStore, runs in the background so it never delays host startup, and swallows failures (a failed warm-up only means the first requests are as slow as they would have been without it). Leave it off in development (fast restarts matter more than the first request) and consider turning it on in production.

Genie:Migration — runtime model migration

Section titled “Genie:Migration — runtime model migration”
Key Default Meaning
MigrationExecution Forced No (skip), Yes (apply only files whose content hash changed), Forced (re-apply everything). Use Yes in production.
ModelsPath null Where the model files live. Absolute path, or relative — see below. null resolves by convention.
FilePatterns *.entity.xml, *.view.xml, *.sql, *.navbar.xml Which kinds of model file the sweep picks up. *.rbac.xml is not swept unless you add it — see below.
MaxRetryAttempts 3 Retries per file for transient failures.
InitialRetryDelayMs 500 First retry delay (doubles per attempt).
MaxDegreeOfParallelism 4 Parallel file processing per category.
ContinueOnError true Keep migrating other files when one fails.
ValidateBeforeExecution true Validate model files before executing them.

ModelsPath resolution. An absolute path is used as-is. A relative path is resolved against the application base directory first (bin/…, matching the copy-to-output shape), then the host content root — so "models" works both for a published app and when running from the source tree.

A configured path that does not exist is logged as an error and migration is skipped: it deliberately does not fall back to the conventional locations, because a typo silently migrating some other folder is worse than a visible stop. Leave ModelsPath unset to use the convention — models/ next to the app binaries, then ../models — which logs a warning and skips if neither exists. See Model migration.

FilePatterns and RBAC. The default omits *.rbac.xml. A baseline RBAC import makes the file the complete grant set for every role it names, so a file that mentions a framework-seeded role (Admin, AccessManager) revokes whatever the engine seeded for it — including the grants that let that role administer access at all. That is a reasonable thing to do deliberately and a bad thing to do by accident, so sweeping it is opt-in:

"Genie": {
"Migration": {
"FilePatterns": [ "*.entity.xml", "*.view.xml", "*.sql", "*.navbar.xml", "*.rbac.xml" ]
}
}

Configuring the list replaces the default rather than adding to it, and a pattern the engine has no strategy for is rejected at startup instead of silently collecting files nothing can execute. The effective list is logged with the models directory, so “why were my permissions not applied” is answered by the startup log. Hosts that would rather deploy RBAC explicitly can leave it out and use POST /api/v1/genie/auth/import-rbac (preview first) or the GenieClient import command.

Also remember the build side: the model files have to reach the output directory. A host that copies models with an explicit glob (as Inventory.Sample.Api.csproj does) must list *.rbac.xml there too — a kind the glob omits is simply absent at runtime, with nothing to say so.

Key Default Meaning
Type Local Local or S3 (covers AWS S3, MinIO, and other S3-compatible stores).
Path "" Local: base directory (absolute, or relative to wwwroot).
AccessUrl "" S3: endpoint URL, e.g. https://s3.amazonaws.com or your MinIO host.
BucketName "" S3: default bucket (also allowlisted for the Object Explorer).
ExplorerBucketNames [] S3: extra buckets the Object Explorer may operate on.
AccessKey / SecretKey "" S3 credentials — prefer env vars (Genie__Storage__SecretKey).
Region "us-east-1" S3 region.
ForcePathStyle true Path-style addressing (required for MinIO).
PublicBaseUrl "" Public base for download links; empty ⇒ AccessUrl (S3) or relative (Local).

Fluent override: ConfigureStorage(s => …).

Genie:Notifications:Email — outbound email channel

Section titled “Genie:Notifications:Email — outbound email channel”
Key Default Meaning
Enabled false false ⇒ the email worker isn’t registered.
SmtpHost / SmtpPort "" / 587 SMTP server.
EnableSsl true STARTTLS/SSL.
FromEmail / FromName "" Sender identity.
Username / Password "" SMTP credentials.
BatchSize 50 Emails per drain batch.
ProcessingInterval 30 Seconds between drain cycles.
MaxRetryAttempts 3 Per-message delivery retries.
TimeoutSeconds 30 SMTP timeout.
IgnoreSslErrors false Only for connecting by IP to hosts with mismatched certs.
Imap:Host / Imap:Port "" / 993 Optional: save sent copies via IMAP.
Imap:SentFolderName "Sent" Target folder for sent copies.

The channel counts as valid (worker eligible) when SmtpHost, FromEmail, and Username are set and SmtpPort > 0. Fluent override: ConfigureEmail(m => …).

Genie:Notifications:Sms — outbound SMS channel

Section titled “Genie:Notifications:Sms — outbound SMS channel”
Key Default Meaning
Enabled false false ⇒ the SMS worker isn’t registered.
Provider "" Provider name, e.g. "Twilio".
AccountSid / AuthToken "" Provider credentials.
FromNumber "" E.164 sender number.
BatchSize / ProcessingInterval / MaxRetryAttempts 50 / 30 / 3 Drain loop tuning.

Valid when Provider and FromNumber are set. Fluent override: ConfigureSms(s => …).

Genie:Notifications:Vapid — browser push keypair

Section titled “Genie:Notifications:Vapid — browser push keypair”
Key Default Meaning
PublicKey / PrivateKey "" VAPID keypair. Without both, web-push delivery is skipped (in-app notifications still work).
Subject mailto:admin@example.com Contact URI sent to the push service.

The public key is served to the browser by GET /api/v1/push-subscription/vapid-public-key; the private key is a secret — supply it as Genie__Notifications__Vapid__PrivateKey.

Genie:Warehousing — replication to external stores

Section titled “Genie:Warehousing — replication to external stores”

Replicates entity rows out to an analytical database (ClickHouse) and/or a search index (Meilisearch). The write-side counterpart to Genie:ReportingSources: that section names databases the engine only ever reads, this one names stores it writes. A deployment that warehouses into ClickHouse and then reports off it configures both, pointed at the same server.

"Genie": {
"Warehousing": {
"Enabled": true,
"Strategy": "Polling",
"PollIntervalSeconds": 30,
"BatchSize": 5000,
"Targets": {
"analytics": {
"Provider": "ClickHouse",
"ConnectionString": "Host=localhost;Port=8123;Database=genie;Username=default;Password=",
"Database": "genie",
"Entities": [ "*" ],
"ExcludeEntities": [ "AuditLog" ]
},
"search": {
"Provider": "Meilisearch",
"Url": "http://localhost:7700",
"Key": "masterKey",
"Entities": [ "*" ]
}
}
}
}
Key Default Meaning
Enabled false Master switch. No target is registered and no worker runs while this is false.
Strategy Polling App-level delivery strategy, inherited by every entity that does not override it with WarehousingStrategy. Polling is the only one implemented.
PollIntervalSeconds 30 Idle wait between sync cycles. A cycle that fills a batch loops again after 1s instead, so a backlog drains fast.
BatchSize 5000 Rows read per cycle per entity. A batch is held as positional arrays, not a dictionary per row, so this stays cheap; lower it only for unusually wide rows.
WatermarkLagSeconds 60 How long a gap in a Watermark entity’s key sequence may persist before it is stepped over. Must exceed the longest transaction writing to that table — see Warehousing.
Targets {} The destinations, keyed by a host-chosen name that identifies them in logs and on the hosted-services dashboard. Case-insensitive.

Per target:

Key Default Meaning
Provider — ClickHouse or Meilisearch.
Enabled true Park a target without deleting its configuration (and its credentials).
ConnectionString — ClickHouse. A literal connection string, never a ConnectionStrings key — a warehousing target is written to, and a name that silently resolved elsewhere would replicate production rows into the wrong store.
Database from the connection string ClickHouse. The database holding the replicated tables.
Url — Meilisearch. e.g. http://localhost:7700.
Key — Meilisearch. API key. Needs write access — this target both writes documents and backs the search query path.
Entities ["*"] Which entities this target receives. "*" (or omitting the key) means every warehoused entity; a name list means only those. Matched case-insensitively against the entity name or its ObjectType.
ExcludeEntities [] Subtracted after Entities is applied, and wins on a tie.

An entity opts into replication in its model — a Warehousing strategy or a <Search> block; see Entities → Warehousing. A target’s Entities selects from that set and can never widen beyond it. A Meilisearch target additionally skips entities with no <Search> block, since the document shape comes from that config.

Fluent overrides: ConfigureWarehousing(w => …) for the cadence, and UseWarehousingTarget(name, target) to add or replace one target the way UseSource does. See Warehousing and Global search.

Identity — Genie:Auth (read by AddGenieAuth / AddGenieApp)

Section titled “Identity — Genie:Auth (read by AddGenieAuth / AddGenieApp)”

One unified section binds the whole auth graph, and it is the only path — the pre-consolidation scattered sections (Security, Authentication:MFA, Authentication:JwtSettings, Authorization, PasswordEncryption) are rejected at startup, not read. Fluent ConfigureAuth(auth => …) calls win over configuration. The minimal default is username/password + JWT + cookie session; every heavier feature is opt-in.

Key Default Meaning
Issuer / Audience "Zed" / "ZedClient" Token claims — set to your app’s values.
AccessTokenExpiryMinutes 60 Access-token lifetime.
RefreshTokenExpiryDays 7 Refresh-token lifetime (needs Features:RefreshTokens).
KeyPath %ProgramData%\Genie\jwt-keys Directory the signing keypair is persisted to under JwtKeyStorage.FileSystem. Environment variables are expanded. Ignored for CacheStore.
Key Default Meaning
CookieName "Genie.Engine.Auth" Session cookie name.
SlidingExpiration true Renew the cookie on activity.

The cookie is always HttpOnly, SameSite=Lax, Secure — not configurable.

Key Default Meaning
MaxInvalidLoginAttempts 5 Failed logins before lockout.
UserLockedTimeoutMinutes 5 Lockout duration; 0 = locked until an admin unlocks.
PasswordExpiryDays 180 0 disables expiry. Counted from the seeded system user’s first boot, not from a fixed date.
PasswordHistoryCount 3 Recent passwords that can’t be reused; 0 disables.
SessionTimeoutMinutes 480 Cookie session lifetime (8 h).
AllowConcurrentSessions false false = a new login revokes the previous session’s tokens.
WebLoginBlockedRoles [] Roles blocked from the web UI (API/JWT still works).
DetailedAuthErrors false Surface precise failure reasons — never in production.
PasswordRequirements:MinPasswordLength 8 Plus RequireDigit/RequireLowercase/RequireUppercase (true) and RequireNonAlphanumeric (false).
Key Default Meaning
Email:Enabled true Email OTP channel.
Email:UseProductionCodes false false means the dev code 000000 is accepted — set true in production.
Email:OtpExpiryMinutes 10 OTP lifetime.
Sms:Enabled false SMS OTP (needs a valid Genie:Notifications:Sms channel).
GoogleAuth:Enabled false TOTP authenticator apps.
GoogleAuth:Issuer "Enfra" Name shown in the authenticator app — set to your app.
GoogleAuth:WindowSize 1 Clock-drift tolerance in 30 s steps.
MaxValidationAttempts / ChallengeTtlMinutes 5 / 10 Challenge limits.
MaxResends / ResendCooldownSeconds / VerificationLockoutSeconds 3 / 120 / 120 Resend + lockout tuning (0 disables).

Genie:Auth:Features — opt-in flows (all default false)

Section titled “Genie:Auth:Features — opt-in flows (all default false)”
Key Enables
RefreshTokens Rotating refresh tokens with reuse detection (Redis-backed).
PasswordReset Forgot-password / magic-link flow (see Genie:Auth:PasswordReset: CooldownSeconds = 120, MagicLinkTtlMinutes = 30).
Impersonation System-role impersonation (POST /api/v1/impersonation/start|stop).

Equivalent fluent calls: auth.AddRefreshTokens(), auth.AddPasswordReset(), auth.AddImpersonation().

  • Recaptcha — Enabled (false), SiteKey, SecretKey. Off = validation short-circuits to success.
  • Login — ShowSignUpLink (false).
  • Authorization — AllowUndefinedResources (false): whether resources without permission definitions are open to all authenticated users (true) or denied (false).
  • PasswordEncryption — the database password-encryption secrets, detailed below.

Code-only auth knobs (fluent builder, not appsettings)

Section titled “Code-only auth knobs (fluent builder, not appsettings)”
  • auth.UseJwtKeyStorage(JwtKeyStorage.FileSystem | JwtKeyStorage.CacheStore) — where the RS256 signing keypair lives. CacheStore (Redis) is required for multi-node hosts and must be paired with Genie:DataProtection Enabled: true + Provider: "Redis" (see the pairing rule).
  • auth.EnableAntiforgery — global CSRF filter for cookie-based requests, on by default.
Key Required Meaning
SeedAdminPassword recommended First-boot password for the seeded system user. Defaults to Admin@123 with a logged warning — always set it.
MasterKeyPassword yes Database-side password encryption: the SQL Server master-key password, or the source PostgreSQL derives its AES-256 key from. Missing ⇒ startup exception.
PasswordEncryptionKey legacy PostgreSQL-only fallback (base64 of 32 bytes) used when MasterKeyPassword is absent.

These are secrets — supply them via environment variables (Genie__Auth__PasswordEncryption__MasterKeyPassword=…) or a secret store in production.

The seeded system user’s password clock starts on first boot: the same run-once engine-SQL step that encrypts SeedAdminPassword also stamps PasswordLastChangedAt, so the account is never expired on arrival regardless of when you deploy. A database seeded by an older engine version kept a fixed seed date and may already be past PasswordExpiryDays — that shows up as a login returning RequiresPasswordChange: true with no tokens, which also blocks non-interactive clients. Reset it once:

-- PostgreSQL
UPDATE "Identity"."Users" SET "PasswordLastChangedAt" = now(), "PasswordExpiryTime" = NULL WHERE "Id" = 1;
-- SQL Server
UPDATE [Identity].[Users]
SET [PasswordLastChangedAt] = CAST(SYSUTCDATETIME() AS DATETIMEOFFSET), [PasswordExpiryTime] = NULL
WHERE [Id] = 1;

Genie:DataProtection (read by AddGenieAuth)

Section titled “Genie:DataProtection (read by AddGenieAuth)”
Key Default Meaning
Enabled false false = framework-default (possibly ephemeral) key ring.
Provider "FileSystem" "Redis" (shared, multi-node — needs ConnectionStrings:Redis) or "FileSystem".
ApplicationName "Zed" Key-isolation scope — always set it (the framework default is the content-root path, so moving the folder breaks decryption).
KeyPath C:\ProgramData\Zed\dp-keys FileSystem provider directory (note: the property is KeyPath, not Path).
RedisKey "DataProtection:Keys" The Redis key the ring is stored under (a cache key, not a config path).
UseDpapi true DPAPI-NG at-rest encryption (Windows + FileSystem only).

Full failure-mode discussion: Backend → Data Protection.

Genie:Hangfire (read by AddGenieHangfire / AddGenieApp)

Section titled “Genie:Hangfire (read by AddGenieHangfire / AddGenieApp)”
Key Default Meaning
RedisPrefix "hangfire" Key prefix (: auto-appended) — set per app when sharing Redis.
RedisDatabase (connection default) Redis DB index override.
WorkerCount (Hangfire default) Processing workers (ProcessorCount × 5).
KnownQueues default, pipelines, transfers, notifications, logs Queues the server processes / the dashboard offers.
DefaultPageSize / MaxJobsPerStateQuery 25 / 1000 Jobs-dashboard paging limits.

No ConnectionStrings:Redis ⇒ only the dashboard service registers and the Jobs API returns 503; the rest of the engine boots normally.

Genie:Assistant (read by AddGenieAssistant / AddGenieApp)

Section titled “Genie:Assistant (read by AddGenieAssistant / AddGenieApp)”
Key Default Meaning
Providers[].Provider "Ollama" Per model entry: OpenAI | DeepSeek | Gemini | Ollama | OpenAICompatible | ChatClient (case-insensitive; unknown throws at startup). See Configuration & providers.
Providers[].Endpoint kind default The API root. Optional for the hosted kinds (each defaults to its own service), required for OpenAICompatible. A trailing /chat/completions is accepted and stripped.
Providers[].Model "codellama:7b" Model name as the provider knows it. Any name the provider serves is accepted.
Providers[].ApiKey null Required for OpenAI/Gemini/DeepSeek. Supply out-of-band as Genie__Assistant__Providers__{n}__ApiKey.
Providers[].MaxTokens / Temperature / TopP 10000 / 0.1 / 0.9 Generation tuning, per model.
Providers[].TimeoutSeconds 60 Timeout for a single provider HTTP request, per attempt (failed transport / 408 / 429 / 5xx attempts are retried twice).
RequestTimeoutSeconds 120 Overall deadline for one assistant turn (MCP loop + streamed reasoning read); on elapse the caller is told the assistant timed out. 0 disables.
ConversationMode true Multi-turn chat vs single-query.
ConversationHistoryLimit 3 Prior turns sent per request (0 = full history).
ChatWidgetEnabled false The in-app chat launcher.
ChatWidgetEnabledForRoles [] Role allowlist; "*" or empty = all authenticated users.
ConnectionString null Optional read-only DB for assistant query execution.
DocsPath null Directory of *.md app-help docs injected as context.

More keys (memory, titles, welcome message) in Assistant.

Key Default Meaning
Genie:OpenApi:Title / Version / Description "Genie API" / "v1" / "API documentation" Swagger document metadata.
Genie:Diagnostics:AllowedIPs [] IPs allowed to reach Swagger / OpenAPI / the Hangfire dashboard (loopback and the server’s own addresses always pass). Everyone else gets 404 via UseGenieDiagnosticsAccess().
Section Keys Defaults Applied to
Genie:RateLimiting:Auth PermitLimit, WindowMinutes 20 / 1 Login and auth endpoints, per source IP.
Genie:RateLimiting:Otp PermitLimit, WindowMinutes 5 / 1 OTP send/verify endpoints, per source IP.

Both policies are opt-in at the host: they only take effect once the host registers them (limiter.AddGenieAuthPolicy(configuration) / AddGenieOtpPolicy(configuration) inside AddRateLimiter) and calls app.UseRateLimiter().

Genie:ForwardedHeaders (behind a reverse proxy)

Section titled “Genie:ForwardedHeaders (behind a reverse proxy)”
Key Default Meaning
Enabled false Honour X-Forwarded-For / X-Forwarded-Proto from the proxy.
KnownProxies / KnownNetworks [] Trusted proxy IPs / CIDR ranges (empty = loopback only).
ForwardLimit 1 Max forwarded entries processed.

Enable this when running behind the reverse proxy so rate limiting and audit see real client IPs.

Background workers — config-driven, or explicit in code

Section titled “Background workers — config-driven, or explicit in code”

There is no worker config section. By default, worker registration follows the channel config: app-notifications always run; the email/SMS workers run when their channel is Enabled and valid; the warehousing sync worker runs when at least one warehousing target is valid. To split an API host from a dedicated worker host, switch to explicit opt-in in code:

genie.AddWorkers(w => w.AddAppNotificationWorker().AddEmailWorker());

See Backend → Background workers.

The defaults are development-friendly; flip these before going live:

  • Genie:Errors:ExposeErrorDetails → false (5xx messages hidden).
  • Genie:Auth:PasswordEncryption:SeedAdminPassword → a real secret (and rotate the seeded account’s password).
  • Genie:Auth:PasswordEncryption:MasterKeyPassword → set (required) via env var / secret store.
  • Genie:Migration:MigrationExecution → Yes (hash-checked) instead of Forced.
  • Genie:Auth:Mfa:Email:UseProductionCodes → true if email MFA is on (kills the 000000 dev code).
  • Genie:Auth:Jwt:Issuer/Audience → your app’s values.
  • Genie:DataProtection → Enabled: true, ApplicationName set; Provider: "Redis" for multi-node or CacheStore JWT keys.
  • Genie:Spa:Enabled → true with RootPath at the published UI (one deployable), or front both with a reverse proxy — either way, avoid cross-origin.
  • Genie:Cors:AllowedOrigins → keep empty unless a UI genuinely lives on another domain.
  • Genie:Diagnostics:AllowedIPs → set, and add app.UseGenieDiagnosticsAccess() so Swagger/Hangfire are hidden from the public.
  • Genie:ForwardedHeaders:Enabled → true with your proxy in KnownProxies when behind one — plus the two host calls it needs (see above).
  • Environment-specific files (appsettings.Production.json) and __-separated env vars use the Genie: paths too — a stale legacy path now fails startup, so verify the app boots.