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" ] } }}Renamed sections
Section titled “Renamed sections”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.
Genie:Datasource
Section titled “Genie:Datasource”| 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.
ConnectionStrings
Section titled “ConnectionStrings”| 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.
Genie:Errors — error-detail exposure
Section titled “Genie:Errors — error-detail exposure”| 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.
Genie:Spa — same-origin UI hosting
Section titled “Genie:Spa — same-origin UI hosting”| 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.
Genie:Cors — cross-origin policy
Section titled “Genie:Cors — cross-origin policy”| 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. |
Genie:Uploads — upload limits
Section titled “Genie:Uploads — upload limits”| 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.
Genie:Performance — performance logging
Section titled “Genie:Performance — performance logging”| 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 sameActivity.Current?.Id ?? TraceIdentifierthe 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.DiagnosticsTo 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);Genie:Warmup — startup warm-up
Section titled “Genie:Warmup — startup warm-up”| 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.
Genie:Storage — file/object storage
Section titled “Genie:Storage — file/object storage”| 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.
Genie:Auth:Jwt
Section titled “Genie:Auth:Jwt”| 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. |
Genie:Auth:Cookies
Section titled “Genie:Auth:Cookies”| 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.
Genie:Auth:Security
Section titled “Genie:Auth:Security”| 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). |
Genie:Auth:Mfa
Section titled “Genie:Auth:Mfa”| 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().
Other Genie:Auth subsections
Section titled “Other Genie:Auth subsections”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 withGenie:DataProtectionEnabled: true+Provider: "Redis"(see the pairing rule).auth.EnableAntiforgery— global CSRF filter for cookie-based requests, on by default.
Secrets — Genie:Auth:PasswordEncryption
Section titled “Secrets — Genie:Auth:PasswordEncryption”| 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:
-- PostgreSQLUPDATE "Identity"."Users" SET "PasswordLastChangedAt" = now(), "PasswordExpiryTime" = NULL WHERE "Id" = 1;
-- SQL ServerUPDATE [Identity].[Users]SET [PasswordLastChangedAt] = CAST(SYSUTCDATETIME() AS DATETIMEOFFSET), [PasswordExpiryTime] = NULLWHERE [Id] = 1;Platform modules
Section titled “Platform modules”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.
Genie:OpenApi + Genie:Diagnostics
Section titled “Genie:OpenApi + Genie:Diagnostics”| 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(). |
Genie:RateLimiting
Section titled “Genie:RateLimiting”| 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.
Production checklist
Section titled “Production checklist”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 ofForced. -
Genie:Auth:Mfa:Email:UseProductionCodes→trueif email MFA is on (kills the000000dev code). -
Genie:Auth:Jwt:Issuer/Audience→ your app’s values. -
Genie:DataProtection→Enabled: true,ApplicationNameset;Provider: "Redis"for multi-node orCacheStoreJWT keys. -
Genie:Spa:Enabled→truewithRootPathat 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 addapp.UseGenieDiagnosticsAccess()so Swagger/Hangfire are hidden from the public. -
Genie:ForwardedHeaders:Enabled→truewith your proxy inKnownProxieswhen behind one — plus the two host calls it needs (see above). - Environment-specific files (
appsettings.Production.json) and__-separated env vars use theGenie:paths too — a stale legacy path now fails startup, so verify the app boots.