Configuration - BaryoDev/barakoCMS GitHub Wiki
Every setting barakoCMS reads. Written from the 4.0.1 source, then brought up to 4.6.0: the keys
added in 4.5.0 and 4.6.0 (tenancy mode, tenant domains in CORS, tracing, workflow concurrency and
email attachments, configured rate limit policies, OpenID Connect sign-in, form email verification
and per-form limits, import caps) were checked against the v4.6.0 tag. Each key is given in
config form (Jwt:Key style, what you put in appsettings.json) and environment form (replace each
: with a double underscore). An array element takes an index: ForwardedHeaders__KnownNetworks__0.
Start here: Home. For what changed in this major, see barakoCMS 4.0.
Three defaults account for most first-deployment surprises. All three are verifiable in the source and each is covered in its own section below.
-
JWT__Keymust be 32 characters or more, or the process exits at startup. A value starting withREPLACE_THISis also refused, because the shipped k8s secret carries a length-valid placeholder. -
Seed__DemoContentis on in Development and off everywhere else. Turning it on seeds a demo content type and an active workflow that sends email, so in production it adds an outbound mail path. -
The refresh cookie is marked
Secureoutside Development. A Production API served over plain HTTP cannot keep a browser session, because the browser will not send the cookie back.
Ordinary .NET configuration: appsettings.json, then appsettings.{Environment}.json, then
environment variables. The shipped defaults live in
barakoCMS/appsettings.json,
and appsettings.Development.json overrides only Swagger:Enabled.
Two things are not ordinary:
Modules see only their own section. A module is handed Modules:{Name} and nothing else, so the
connection string and the signing key are out of its reach
(ModuleConfiguration).
Some modules also declare a legacy root section they used to read; when the scoped section is empty
and the legacy one is not, the legacy one is used and a warning names both. When both exist the keys
are merged and the scoped value wins. The module tables below give the scoped key and the legacy key
where one exists.
One key can be overridden from the database. Kubernetes__Enabled is read through
IConfigurationService, which checks a SystemSetting document first, then configuration, then the
default
(ConfigurationService.cs).
Nothing else in the core uses that path.
SKIP_SEEDER is read straight from the environment, not from configuration. Any non-empty value
skips the startup seeder entirely
(Program.cs).
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
ConnectionStrings:DefaultConnection |
ConnectionStrings__DefaultConnection |
Postgres connection string | empty | Yes outside Development. In Development a dummy string is used so design-time tooling works |
DATABASE_URL |
DATABASE_URL |
postgres:// URL, parsed into a connection string; wins over DefaultConnection
|
unset | No |
Tenancy:DatabaseEnforcement |
Tenancy__DatabaseEnforcement |
Row level security policies per tenant | false |
Yes when on and the app connects as a Postgres superuser, since a policy cannot bind a superuser |
EventSourcing:DocumentTypesAppend |
EventSourcing__DocumentTypesAppend |
Whether document content types also append events | true |
No |
DATABASE_URL defaults sslmode to Require and refuses an unrecognised sslmode. Npgsql's
IncludeErrorDetail is on only in Development on that path, so parameter values do not reach logs in
production
(ResolveConnectionString).
Schema handling is not configurable: Development uses Marten's CreateOrUpdate, everything else uses
CreateOnly, which creates missing objects and never alters an existing one.
Migrations have no setting of their own either. Since 4.6.0 the shipped SQL files are applied by
db-migrate, an argument to the image like db-assert, run with the API stopped. It records each
file in the public.barako_migrations ledger table and connects the way the app does, so give it the
same ConnectionStrings__DefaultConnection and JWT__Key the deploy uses. A module's files run only
where that module is enabled. See
docs/migrations.md.
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
JWT:Key secret
|
JWT__Key |
Signing key for access tokens, preview tokens and the MFA challenge token | none |
Yes. Missing, under 32 characters, or starting with REPLACE_THIS all throw |
JWT:Issuer |
JWT__Issuer |
Issuer claim, validated on every token | https://localhost:7049/ |
No |
JWT:Audience |
JWT__Audience |
Audience claim, validated on every token | https://localhost:7049/ |
No |
Auth:RequireEmailVerification |
Auth__RequireEmailVerification |
Self-registration must prove its address | true |
Yes when set to false without the acknowledgement below |
Auth:AcknowledgeUnverifiedRegistration |
Auth__AcknowledgeUnverifiedRegistration |
Confirms you meant to turn verification off | false |
See above |
Auth:LegacyRoleFallback |
Auth__LegacyRoleFallback |
Fall back to role names when a capability check fails. Since 4.6.0 this also lets the role names Admin and SuperAdmin read and delete other users' private files, where manage_all_files decides otherwise |
false |
No |
Roles:RefuseUnknownCapabilities |
Roles__RefuseUnknownCapabilities |
Reject a role write naming a capability this instance does not know | false |
No |
DeviceTrust:Enforce |
DeviceTrust__Enforce |
Password sign-in requires a trusted device (DeviceTrust module) | false |
No |
Branding:AppName |
Branding__AppName |
Name used in OTP issuers and security emails | BarakoCMS |
No |
JwtKeyGuard.cs
is the whole rule, and it runs from AddBarakoCMS:
public const int MinLength = 32;
if (string.IsNullOrWhiteSpace(key) || key.Length < MinLength)
{
throw new System.InvalidOperationException(
$"JWT:Key must be configured and at least {MinLength} characters (256 bits) for security.");
}The second check refuses any key beginning REPLACE_THIS. The reason is in the same file: the shipped
k8s/02-secret.yaml placeholder is 45 characters, so an operator who applies the manifests unedited
would otherwise boot on a key published in this repository.
The key is read as JWT:Key first and then as the JWT__Key environment variable directly
(ServiceCollectionExtensions.cs),
so both forms work even where the environment provider is not wired up.
Sign-in sets barako_refresh, HttpOnly, SameSite=Lax, scoped to /api/auth/refresh. Whether it
carries Secure is decided by the environment, not by the scheme of the request:
internal static bool IsSecure(HttpContext http) =>
!http.RequestServices.GetRequiredService<IHostEnvironment>().IsDevelopment();The file explains why it does not follow Request.IsHttps: behind a TLS-terminating proxy that is not
forwarding headers, an HTTPS request arrives as HTTP and the cookie would ship without Secure on the
deployment that needs it most.
The practical consequence for you: a Production or Staging API reached over plain http:// sets a
Secure cookie the browser will not send back, so browser sessions cannot refresh. Either put TLS in
front of it, or run the API in Development. The refresh token is also in the response body, so
non-browser callers are unaffected.
All four are secrets. None of them belongs in the repository.
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
Secrets:Key secret
|
Secrets__Key |
Key material for credentials an admin typed into the API (the email API key) | falls back to JWT:Key
|
No, because of the fallback |
Mfa:Key secret
|
Mfa__Key |
Key material for stored TOTP secrets | falls back to JWT:Key
|
No, because of the fallback |
Connectors:Key secret
|
Connectors__Key |
Key material for connector credentials. Unset means connector credentials cannot be encrypted and those endpoints refuse | unset | Yes when set to fewer than 32 characters, or to the same value as JWT:Key, Mfa:Key or Secrets:Key
|
Metrics:ScrapeKey secret
|
Metrics__ScrapeKey |
Shared key for /metrics, sent as Authorization: Bearer or X-Metrics-Key
|
unset, and unset means /metrics serves nobody |
No |
Rotating any of these makes everything already encrypted under it unreadable. That is why
Connectors:Key is refused when it duplicates another key: one rotation would otherwise retire two
unrelated controls
(ConnectorOptions.cs).
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
CORS:AllowedOrigins |
CORS__AllowedOrigins |
Comma-separated browser origins allowed with credentials | empty. Outside Development, no configured origins means no cross-origin access at all. In Development an empty value falls back to http://localhost:3000,http://localhost:3001,https://localhost:7049
|
No |
CORS:AllowTenantDomains |
CORS__AllowTenantDomains |
Also allow https:// plus a domain an active tenant holds, or its www. form, matched exactly. Such an origin never gets credentials. Anything but a true value leaves it off |
false |
No |
AllowedHosts |
AllowedHosts |
Semicolon-separated Host headers the API answers to |
* (any) |
No |
App:BaseUrl |
App__BaseUrl |
This deployment's public origin, used for every absolute link the API hands out | empty | No when unset, but with AllowedHosts at * the feed and sitemap then answer 503 instead of guessing an origin. A malformed value throws per request instead; see below |
ForwardedHeaders:Enabled |
ForwardedHeaders__Enabled |
Read X-Forwarded-For and X-Forwarded-Proto
|
false |
Yes when on and neither known-proxy list is set |
ForwardedHeaders:KnownProxies |
ForwardedHeaders__KnownProxies__0 |
Trusted proxy IP addresses | empty | A non-IP value throws |
ForwardedHeaders:KnownNetworks |
ForwardedHeaders__KnownNetworks__0 |
Trusted proxy networks in CIDR form | empty | A non-CIDR value throws |
ForwardedHeaders:ForwardLimit |
ForwardedHeaders__ForwardLimit |
How many hops to trust | 1 |
No |
Hsts:MaxAgeDays |
Hsts__MaxAgeDays |
HSTS max-age, applied by UseHsts outside Development |
90 |
Yes outside Development when zero or negative, since that would switch HSTS off while looking like tightening. A Development host starts, because the check never runs |
Hsts:IncludeSubDomains |
Hsts__IncludeSubDomains |
Adds includeSubDomains
|
false |
No |
Multitenancy:RefuseUnknownHosts |
Multitenancy__RefuseUnknownHosts |
Refuse a host matching no tenant instead of serving the default tenant | false |
No |
Multitenancy:CacheDuration |
Multitenancy__CacheDuration |
How long the domain-to-tenant map is cached | 5 minutes | No |
Tenancy:Mode |
Tenancy__Mode |
Single or Multi. In Multi only registered, active tenants are served: a request naming no tenant, an unknown slug or an inactive tenant gets 404, and the default partition or an unregistered slug gets no token and accepts no API key. Register a tenant and join it before switching |
Single |
Yes for a value that is not a mode |
Before 4.3.0 the localhost fallback applied in every environment, so a production deployment that forgot this key accepted credentialed requests from those three localhost ports. A deployment with a browser client now has to set it, and one without sees a CORS error rather than a hole.
CORS:AllowedOrigins also governs which response headers scripts may read: ETag,
X-Api-Contract-Version and X-Delivery-Contract-Version are exposed to every allowed origin.
CORS:AllowTenantDomains is off by default, so the allowed origins stay exactly
CORS:AllowedOrigins until it is set. A tenant domain is https only, with no port, path, wildcard,
IP literal or other subdomain. With it on, responses carry Vary: Origin, and a domain saved on a
tenant passes on that instance at once and on others within Multitenancy:CacheDuration
(TenantDomainCorsPolicyProvider.cs).
Leaving AllowedHosts at * is what makes App:BaseUrl necessary. With host filtering wide open
the API will not build links from the caller's Host header, so it returns nothing rather than let
the caller choose the origin
(CanonicalHost.cs).
Unset and malformed are two different failures. Unset is the 503 above, with a message naming the key
to set. A value that is set but is not an absolute http or https URL throws
InvalidOperationException instead, and the same check covers Feeds:SiteUrl. api.example.com with
no scheme is the one to watch for: it parses as a relative URI, so it throws rather than being read as
a host. The throw happens inside the request, not at startup, so the feed 500s while the host looks
healthy. The message names the key and the value.
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
InitialAdmin:Username |
InitialAdmin__Username |
Username of the seeded SuperAdmin. Read only on an empty user table | admin |
No |
InitialAdmin:Password secret
|
InitialAdmin__Password |
Its password, same condition. Empty means one is generated and printed once to the container console | empty | No |
Seed:DemoContent |
Seed__DemoContent |
Seed the demo content type, records and workflow | on in Development, off everywhere else | No |
| (environment only) | SKIP_SEEDER |
Any non-empty value skips the seeder | unset | No |
SeedUsersAsync counts users before it reads anything:
var userCount = await session.Query<User>().CountAsync();
if (userCount > 0)
{
Console.WriteLine("[DataSeeder] Users already exist, skipping user seeding");
return;
}The InitialAdmin section is read after that return, so on a database with even one user both keys
are ignored. Changing InitialAdmin__Password on a running deployment does nothing, and changing
InitialAdmin__Username neither renames the account nor creates a second one. Reset a forgotten
admin password through the API or the database, not through this key.
The generated-password note has the same condition. It is printed on the boot that creates the
account and never again, it goes to Console.WriteLine rather than the logger so it does not reach a
log sink, and it is not recoverable afterwards.
Roles are seeded on every boot whatever Seed:DemoContent says, and SeedRolesAsync backfills
system capabilities onto roles that already exist. That is separate from the user gate above.
The default comes from DataSeeder.cs:
return configuration.GetValue("Seed:DemoContent", environment.IsDevelopment());When it is on, three things are seeded:
-
AttendanceRecord, a content type withFirstName,LastName,Email,BirthDay,JobDescription,GenderandSSN, whereSSNis marked Sensitive and masked to its last four characters. - Three sample records, deliberately unusable as personal data (
Sample Employee Oneand friends, mail atexample.com, SSN values that readSAMPLE-NOT-A-REAL-SSN-1). - A workflow named
Attendance Confirmation Email, stored active, triggering onAttendanceRecordCreatedwith conditionstatus = Published, whose single action isSendEmailto{{data.Email}}.
That last one is the reason to leave it off in production. Once a mail provider is configured, the
demo fixture is an outbound mail path: any published AttendanceRecord mails whatever address its
Email field holds. The quickstart compose file sets Seed__DemoContent to false explicitly even
though it already runs as Production.
Sample login accounts (hr_manager, john_viewer, with fixed passwords) are separate: they are
seeded only when ASPNETCORE_ENVIRONMENT is Development and the user table is empty, and
Seed:DemoContent does not control them.
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
BarakoCMS:Modules:Enabled |
BarakoCMS__Modules__Enabled |
Which modules run, as an array or a comma-separated string | unset means every module found runs, with one warning. "" means core only |
Yes when a listed name matches no module |
BarakoCMS:Modules:Discover |
BarakoCMS__Modules__Discover |
Scan the dependency context for modules | true |
No |
BarakoCMS:Modules:SchemaPreflight |
BarakoCMS__Modules__SchemaPreflight |
Report, per module, the schema migration Marten would apply | unset means on when the store is CreateOnly, which is every environment except Development |
No |
Do not map BarakoCMS__Modules__Enabled from an optional shell variable. An unset variable arrives
as an empty string, and an empty string means core only. The quickstart compose file leaves it
commented out for exactly this reason.
Resend:* is read at the root by the core, not by the Resend module, so these keys keep their
root-level form.
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
Resend:ApiKey secret
|
Resend__ApiKey (also plain RESEND_API_KEY) |
Resend API key. A key stored through the admin wins over this | unset | No |
Resend:From |
Resend__From |
Default sender address. A stored from address wins | unset | No |
Resend:WebhookSecret secret
|
Resend__WebhookSecret (also plain RESEND_WEBHOOK_SECRET) |
Verifies Resend delivery webhooks | unset, and unset means the webhook answers 401 to everyone | No |
Modules:Email.Smtp:Host |
Modules__Email.Smtp__Host |
SMTP relay hostname. Blank means the module registers no provider at all | unset | No |
Modules:Email.Smtp:Port |
Modules__Email.Smtp__Port |
Submission port | 587 |
No |
Modules:Email.Smtp:User |
Modules__Email.Smtp__User |
Relay username | unset | No |
Modules:Email.Smtp:Password secret
|
Modules__Email.Smtp__Password |
Relay password | unset | No |
Modules:Email.Smtp:From |
Modules__Email.Smtp__From |
Sender address. A from address stored in the admin wins | unset | No |
Modules:Email.Smtp:Security |
Modules__Email.Smtp__Security |
None, StartTls or SslOnConnect
|
unset, which picks implicit TLS on port 465 and STARTTLS elsewhere | No |
With no provider module configured, the core registers a mock email service that logs instead of sending.
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
Modules:Files.S3:Bucket (legacy Files:S3:Bucket) |
Modules__Files.S3__Bucket |
Bucket name. Blank leaves the module dormant and Postgres storage serving | unset | No |
Modules:Files.S3:ServiceUrl |
Modules__Files.S3__ServiceUrl |
Endpoint for R2 or MinIO. Leave unset for AWS | unset | No |
Modules:Files.S3:Region |
Modules__Files.S3__Region |
AWS region, used only when ServiceUrl is unset |
us-east-1 |
No |
Modules:Files.S3:AccessKey secret
|
Modules__Files.S3__AccessKey |
Access key | empty | No |
Modules:Files.S3:SecretKey secret
|
Modules__Files.S3__SecretKey |
Secret key | empty | No |
Modules:Files.S3:ForcePathStyle |
Modules__Files.S3__ForcePathStyle |
Path-style addressing, needed by MinIO and R2 | true |
No |
Modules:Files.S3:PublicBaseUrl |
Modules__Files.S3__PublicBaseUrl |
Public base URL for public objects. Unset proxies them through the API | unset | No |
Modules:Files.S3:UsePublicReadAcl |
Modules__Files.S3__UsePublicReadAcl |
Set a public-read ACL on public objects. Turn off for R2, which ignores object ACLs | true |
No |
Modules:Files:Files:Scanner:Address |
Modules__Files__Files__Scanner__Address |
Registers the clamd scanner. Needed together with the root key below; see the section after this table | empty | No |
Files:Scanner:Address |
Files__Scanner__Address |
host:port the registered scanner connects to. Needed together with the scoped key above |
empty | No |
Files:Scanner:TimeoutSeconds |
Files__Scanner__TimeoutSeconds |
Whole-exchange timeout for a scan, which is also how long an upload waits. Read at the root, like the address | 30 |
No |
Files:Images:MaxWidth |
Files__Images__MaxWidth |
Widest ?w= a caller may ask for. Zero turns variants off |
2048 |
No |
Files:Images:MaxSourcePixels |
Files__Images__MaxSourcePixels |
Largest source image that gets resized at all; larger ones are served unresized | 50000000 |
No |
RequestLimits:MaxBodyBytes |
RequestLimits__MaxBodyBytes |
Kestrel request body limit and multipart limit | 10 MB | No |
Import:MaxExpandedBytes |
Import__MaxExpandedBytes |
Largest uncompressed spreadsheet size an upload may declare | 8 MB | No |
Set both, to the same value:
Modules__Files__Files__Scanner__Address=clamav:3310
Files__Scanner__Address=clamav:3310
They do different jobs, and the reason is the module scoping described at the top of this page.
FilesModule declares no legacy root section, so it is handed Modules:Files and nothing else. The
literal it reads is Files:Scanner:Address, which against that section resolves to
Modules:Files:Files:Scanner:Address. Blank there registers NoFileScanner, set there registers
ClamAvScanner
(FilesModule.cs).
ClamAvScanner then reads the same literal from the root configuration its constructor is given, so
for it the key is plain Files:Scanner:Address, and that read is where it gets a host
(ClamAvScanner.cs).
Either key on its own scans nothing:
- Root key only.
NoFileScanneris registered and itsConfiguredis false. - Scoped key only.
ClamAvScanneris registered, finds no address at the root, and itsConfiguredis false too.
The upload path scans only if (_scanner.Configured), so in both cases it skips scanning and stores
the file. Nothing logs a warning about it. With both keys set, an infected file gets 422 and a
scanner that cannot be reached gets 503, so a test upload tells you which state you are in.
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
Content:Concurrency:Require |
Content__Concurrency__Require |
Require If-Match on a content update, answering 428 without it |
false |
No |
Lifecycle:EnforceTransitions |
Lifecycle__EnforceTransitions |
Refuse a transition whose From state does not match, with 409 |
true |
No |
Lifecycle:AllowSelfTransition:{name} |
Lifecycle__AllowSelfTransition__{name} |
Let the creator of an entry run the named transition on it | false |
No |
PublicDelivery:RequireAcknowledgement |
PublicDelivery__RequireAcknowledgement |
Turning public delivery on for a type needs acknowledgeExposure
|
false |
No |
Sensitivity:Mode |
Sensitivity__Mode |
Off (no scrubbing) or SensitiveOnly (scrub fields marked Sensitive or Hidden). An unparseable value falls back to the default |
SensitiveOnly |
Yes for All, which is declared but not implemented |
Delivery:MaxRadiusKm |
Delivery__MaxRadiusKm |
Widest near radius a public query may ask for; a non-positive value reads as unset |
1000 |
No |
Delivery:Events:Enabled |
Delivery__Events__Enabled |
The anonymous SSE content-change stream | false |
No |
Delivery:Events:MaxConnections |
Delivery__Events__MaxConnections |
Open streams across all tenants; the next gets 503 | 100 |
No |
Delivery:Events:MaxConnectionsPerClient |
Delivery__Events__MaxConnectionsPerClient |
Open streams per client address; zero turns the per-client cap off | 5 |
No |
Delivery:Events:KeepAliveSeconds |
Delivery__Events__KeepAliveSeconds |
Keep-alive interval on a stream | 15 |
No |
Feeds:SiteUrl |
Feeds__SiteUrl |
Front-end origin for feed and sitemap links; preferred over App:BaseUrl
|
unset | No |
Feeds:Paths:{type} |
Feeds__Paths__{type} |
Link template per content type, such as /blog/{slug}
|
/{type}/{slug} |
No |
Feeds:Titles:{type} |
Feeds__Titles__{type} |
RSS channel title for that content type's feed | the type name | No |
Blueprints:Path |
Blueprints__Path |
Directory of custom *.json content-type blueprints, read on every list |
unset | No |
ContentTypes:MaxFields |
ContentTypes__MaxFields |
The most fields one content type may hold, applied when a type is created, when SEO fields are added and on a Portability import. A type already stored with more keeps working; only growth is refused | 200 |
No |
CollectionSyncs:Enabled |
CollectionSyncs__Enabled |
The background sweep that runs collection syncs on their schedule. Off, a sync can still be run from the API | true |
No |
CollectionSyncs:RunLockWaitSeconds |
CollectionSyncs__RunLockWaitSeconds |
How long a run started from the API waits for another run filling the same collection, then answers 409. Held between 0 and 30; 0 answers 409 at once | 5 |
Yes when it is not a number |
Import:MaxRecords |
Import__MaxRecords |
The most records one POST /api/import/content request may hold; more is a 400. Zero or less reads as unset |
5000 |
No |
Portability:MaxImportRecords |
Portability__MaxImportRecords |
The most entries one bundle import may hold. Zero or less reads as unset. The 500 type cap is fixed | 5000 |
No |
Erasure:Mode |
Erasure__Mode |
Delete, None or CryptoShred
|
Delete |
Yes for an unrecognised value, for CryptoShred (not implemented), and for None without the acknowledgement below |
Erasure:AcknowledgeNoErasure |
Erasure__AcknowledgeNoErasure |
Confirms you meant Erasure:Mode=None
|
false |
See above |
Sensitivity:Mode decides whether masking happens, not who sees through it. Since 4.6.0 a Sensitive
value is shown to a role holding view_sensitive, a Hidden one to a role holding view_hidden, and
both to the seeded SuperAdmin role by its id. The role names HR and SuperAdmin no longer count.
That is a role grant, not a setting, so there is no key for it
(SensitivityService.cs).
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
Workflows:RunnerEnabled |
Workflows__RunnerEnabled |
The background workflow runner | true |
No |
Workflows:RunnerConcurrency |
Workflows__RunnerConcurrency |
How many workflow actions one node runs at once. Actions of one run still go one at a time | 1 |
Yes outside 1 to 20 |
Workflows:BacklogIntervalSeconds |
Workflows__BacklogIntervalSeconds |
How often the runner measures the backlog gauges on /metrics. 0 turns the measurement off |
30 |
Yes outside 0 to 3600 |
Workflows:Email:Attachments:MaxCount |
Workflows__Email__Attachments__MaxCount |
Most files one workflow email may attach. 0 turns attachments off | 5 |
No. A value that is not a whole number of zero or more fails every email with attachments, naming the key |
Workflows:Email:Attachments:MaxFileBytes |
Workflows__Email__Attachments__MaxFileBytes |
Largest single attachment | 10 MB | Same as above |
Workflows:Email:Attachments:MaxTotalBytes |
Workflows__Email__Attachments__MaxTotalBytes |
Largest total of one email's attachments | 15 MB | Same as above |
Workflows:Retention:Enabled |
Workflows__Retention__Enabled |
Sweep old workflow runs | true |
No |
Workflows:Retention:Succeeded |
Workflows__Retention__Succeeded |
Days to keep a succeeded run | 7 |
No |
Workflows:Retention:Failed |
Workflows__Retention__Failed |
Days to keep a failed run | 90 |
No |
Webhooks:DeliveryLogRetentionDays |
Webhooks__DeliveryLogRetentionDays |
Days to keep a webhook delivery row; zero or less keeps forever | 30 |
No |
Webhooks:ResponseBodyRetentionHours |
Webhooks__ResponseBodyRetentionHours |
Hours to keep a delivery's response body; zero or less keeps forever | 24 |
No |
Webhooks:AllowProxy |
Webhooks__AllowProxy |
Let outbound webhook calls use a system proxy, which bypasses the outbound address guard | false |
No |
Webhooks:AllowInsecureSignedUrls |
Webhooks__AllowInsecureSignedUrls |
Allow a signed webhook URL over plain HTTP | false |
No |
Jobs:MaxAttempts |
Jobs__MaxAttempts |
Attempts before a job is dead-lettered | 5 |
Throws when below 1, at the point the job queue resolves its options |
Jobs:BackoffBaseSeconds |
Jobs__BackoffBaseSeconds |
Wait after the first failure, doubling after that | 30 |
Throws when negative |
Jobs:BackoffMaxSeconds |
Jobs__BackoffMaxSeconds |
Longest wait between attempts | 3600 |
Throws when below Jobs:BackoffBaseSeconds
|
Jobs:StorageProbeSeconds |
Jobs__StorageProbeSeconds |
How often a worker re-reads storage for due retries and other instances' jobs | 60 |
Throws when below 1 |
Jobs:LeaseSeconds |
Jobs__LeaseSeconds |
How long a claimed job stays claimed, which is also the handler's time limit | 600 |
Throws when below 1 |
Every limit is a fixed window under RateLimiting:{Name} with three keys: PermitLimit,
WindowSeconds and QueueLimit. A zero or negative PermitLimit or WindowSeconds, or a negative
QueueLimit, stops startup with the key named; a limit cannot be turned off by setting it to zero.
Raising Auth or Registration above its default logs a warning at startup.
| Name | Applies to | Partitioned by | Default (permits / window / queue) |
|---|---|---|---|
Global |
every request | client IP | 100 / 60 s / 10 |
Auth |
sign-in, OTP request and verify, MFA verify and disable, token refresh | client IP | 5 / 900 s / 0 |
Batch |
anonymous telemetry: Diagnostics error reports and Pwa install reports | client IP | 20 / 60 s / 0 |
Registration |
self-registration and its verification | client IP | 5 / 3600 s / 0 |
Renderer |
requests carrying the renderer key, in place of Global
|
one shared bucket | 1000 / 60 s / 10 |
SiteShare |
anonymous share link redemption | tenant and visitor | 10 / 60 s / 0 |
Delivery |
the public delivery routes, on top of Global. A request with the renderer key stays in the renderer bucket |
client IP | off until PermitLimit is set, then window 60 s and queue 0 unless set |
ApiKey |
every request an API key authenticated | the API key | off until PermitLimit is set, then window 60 s and queue 0 unless set |
Delivery and ApiKey are opt-in, so an upgrade refuses nothing it served before. Setting their
WindowSeconds or QueueLimit without PermitLimit stops startup, since it would read as a limit
that is on while it is off.
Since 4.6.0 a route in a module or the host can name a limit defined in configuration, with
RequireRateLimiting("{name}"). A policy under RateLimiting:Policies:{name} takes the same three
keys plus Partition. A route naming a policy nobody defined stops startup, naming the route and
the policy. The names auth, telemetry, registration, site-share, delivery, logout and
tls-ask are built in and cannot be used, in any case
(RateLimitSetup.cs).
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
RateLimiting:{Name}:PermitLimit |
RateLimiting__Global__PermitLimit and so on |
Requests allowed per window | see the table above | Yes when zero or negative, or not a whole number |
RateLimiting:{Name}:WindowSeconds |
RateLimiting__Global__WindowSeconds |
Window length | see above | Yes when zero or negative |
RateLimiting:{Name}:QueueLimit |
RateLimiting__Global__QueueLimit |
Requests held for the next window instead of refused | see above | Yes when negative |
RateLimiting:Policies:{name}:PermitLimit |
RateLimiting__Policies__{name}__PermitLimit |
Requests allowed per window for a named policy. WindowSeconds and QueueLimit sit beside it, defaulting to 60 and 0 |
none, it is required | Yes when missing, zero or negative |
RateLimiting:Policies:{name}:Partition |
RateLimiting__Policies__{name}__Partition |
What the policy counts against: Ip, User or ApiKey
|
Ip |
Yes for any other value |
RateLimiting:Renderer:Key secret
|
RateLimiting__Renderer__Key |
A request whose X-Barako-Renderer-Key header matches is counted in the Renderer bucket instead of its IP's global bucket. With it, X-Barako-Visitor-IP names the visitor for SiteShare. Only the global limit and SiteShare honour it; Auth, Batch and Registration stay per IP |
unset, which means no renderer bucket | Yes when set and shorter than 32 characters |
A rejected request gets 429.
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
Swagger:Enabled |
Swagger__Enabled |
The Swagger document and UI, and what GET /api/meta reports |
on when ASPNETCORE_ENVIRONMENT is Development, off otherwise |
No |
Serilog:WriteToFile |
Serilog__WriteToFile |
Also write a daily rolling file under logs/
|
false |
No |
Serilog:MinimumLevel:* |
Serilog__MinimumLevel__Default |
Standard Serilog configuration, read from appsettings.json
|
Information, with Microsoft, System, FastEndpoints, Marten and Npgsql at Warning
|
No |
HealthChecksUI:Enabled |
HealthChecksUI__Enabled |
The health dashboard at /health-ui
|
false |
No |
HealthChecks:MaxPrivateMemoryMegabytes |
HealthChecks__MaxPrivateMemoryMegabytes |
Private-memory ceiling for the memory check | 4096 |
No |
HealthChecks:MinimumFreeDiskMegabytes |
HealthChecks__MinimumFreeDiskMegabytes |
Free-space floor on /
|
512 |
No |
HealthChecks:MaxProjectionLagEvents |
HealthChecks__MaxProjectionLagEvents |
Projection lag tolerated before the workflow projection check reports Degraded | 1000 |
No |
Metrics:ScrapeKey secret
|
Metrics__ScrapeKey |
See Secrets above. Unset means /metrics refuses everyone |
unset | No |
Kubernetes:Enabled |
Kubernetes__Enabled |
Cluster monitoring behind /api/monitoring/k8s. A SystemSetting row of the same key overrides configuration |
false |
No |
Tracing:Otlp:Endpoint |
Tracing__Otlp__Endpoint |
The collector's OTLP endpoint. Setting it turns OpenTelemetry tracing on | unset, which registers no tracer and opens no connection | Yes when set and not an absolute http or https URL |
Tracing:Otlp:Protocol |
Tracing__Otlp__Protocol |
grpc or http/protobuf
|
grpc |
Yes for any other value |
Tracing:Otlp:Headers secret
|
Tracing__Otlp__Headers |
Headers sent with every export, as name=value,name=value
|
unset | No |
Tracing:Otlp:TimeoutSeconds |
Tracing__Otlp__TimeoutSeconds |
How long one export may take before its spans are dropped | 10 |
Yes outside 1 to 60 |
Tracing:ServiceName |
Tracing__ServiceName |
The service.name spans are reported under |
barakocms |
No |
Tracing:SampleRatio |
Tracing__SampleRatio |
Share of traces started here that are recorded. An incoming traceparent decides for itself |
1.0 |
Yes outside 0 to 1 |
Tracing:MaxQueueSize |
Tracing__MaxQueueSize |
Finished spans that may wait for export; a span that finds it full is dropped | 2048 |
Yes outside 1 to 65536 |
A collector that is down costs spans, not requests: export runs on a background thread with the bounded queue and timeout above. The request path, the query string and outbound URLs are not exported. Every attribute a span carries is listed in docs/tracing.md.
/health, /health/live and /health/ready are always mapped and are not configurable. Only the
memory check is tagged live; the database, disk, memory and startup-seeding checks are ready.
| Key | Environment | What it does | Default | Refuses to start without it |
|---|---|---|---|---|
ExternalAuth:Enabled |
ExternalAuth__Enabled |
Master kill switch for social sign-in. Only the literal false disables; unset leaves providers on |
unset | No |
Google:ClientId / Google:ClientSecret secret
|
Google__ClientId / Google__ClientSecret
|
Google sign-in. A provider is on when its client id is set | unset | No |
GitHub:ClientId / GitHub:ClientSecret secret
|
GitHub__ClientId / GitHub__ClientSecret
|
GitHub sign-in | unset | No |
Facebook:AppId / Facebook:AppSecret secret
|
Facebook__AppId / Facebook__AppSecret
|
Facebook sign-in | unset | No |
LinkedIn:ClientId / LinkedIn:ClientSecret secret
|
LinkedIn__ClientId / LinkedIn__ClientSecret
|
LinkedIn sign-in | unset | No |
{Provider}:Enabled |
{Provider}__Enabled |
Set to false to keep one configured provider dark |
unset | No |
Facebook:TrustUnverifiedEmail |
Facebook__TrustUnverifiedEmail |
Treat a Facebook address as verified | false |
No |
Oidc:Providers:{name}:ClientId |
Oidc__Providers__{name}__ClientId |
Any OpenID Connect provider, served at /api/auth/oidc/{name}/start and /callback. A provider is on when its client id is set. {name} is 1 to 32 characters of a-z, 0-9 and hyphen. At most 20 are on, taken in name order |
unset | No. A provider with a client id but a bad Authority or no ClientSecret stays off and is named in the startup log |
Oidc:Providers:{name}:ClientSecret secret
|
Oidc__Providers__{name}__ClientSecret |
Its client secret | unset | See above |
Oidc:Providers:{name}:Authority |
Oidc__Providers__{name}__Authority |
Where the discovery document is fetched. An absolute https URL with no query or fragment |
unset | See above |
Oidc:Providers:{name}:Issuer |
Oidc__Providers__{name}__Issuer |
The issuer the discovery document must name, for a provider whose issuer is a template such as {tenantid}
|
the authority | See above |
Oidc:Providers:{name}:Scopes |
Oidc__Providers__{name}__Scopes |
Space-separated scopes; openid is always added |
openid email profile |
No |
Oidc:Providers:{name}:DisplayName |
Oidc__Providers__{name}__DisplayName |
Name shown for the provider, cut at 100 characters | the provider name | No |
Oidc:Providers:{name}:EmailVerifiedClaim |
Oidc__Providers__{name}__EmailVerifiedClaim |
The id token claim that says the address is verified. It decides which local account a first sign-in lands on | email_verified |
No |
Oidc:Providers:{name}:Enabled |
Oidc__Providers__{name}__Enabled |
Set to false to keep one configured provider dark |
unset | No |
Oidc:RateLimit:PermitLimit |
Oidc__RateLimit__PermitLimit |
Start and callback requests per client IP per window, across OIDC providers. A value that is not a number above zero reads as unset | 20 |
No |
Oidc:RateLimit:WindowSeconds |
Oidc__RateLimit__WindowSeconds |
That window | 300 |
No |
Modules:AI:Enabled (legacy Ai:Enabled) |
Modules__AI__Enabled |
Semantic search module | false |
No |
Modules:AI:EmbeddingBaseUrl |
Modules__AI__EmbeddingBaseUrl |
Embedding server base URL | http://localhost:11434 |
No |
Modules:AI:EmbeddingModel |
Modules__AI__EmbeddingModel |
Embedding model name | nomic-embed-text |
No |
Modules:Analytics.Umami:Enabled (legacy Umami:Enabled) |
Modules__Analytics.Umami__Enabled |
Umami analytics module | false |
No |
Modules:Analytics.Umami:BaseUrl |
Modules__Analytics.Umami__BaseUrl |
Umami instance, server to server | empty | No |
Modules:Analytics.Umami:Username |
Modules__Analytics.Umami__Username |
Umami account used to read stats | empty | No |
Modules:Analytics.Umami:Password secret
|
Modules__Analytics.Umami__Password |
Its password | empty | No |
Modules:Analytics.Umami:PublicUrl |
Modules__Analytics.Umami__PublicUrl |
Public URL of the tracking script, for the copy-paste snippet | falls back to BaseUrl
|
No |
Modules:Forms:PermitLimit |
Modules__Forms__PermitLimit |
Form submissions per client IP per window, across every form | 5 |
No |
Modules:Forms:WindowSeconds |
Modules__Forms__WindowSeconds |
That window | 600 |
No |
Modules:Forms:PerForm:{slug}:PermitLimit |
Modules__Forms__PerForm__{slug}__PermitLimit |
A submit limit of its own for one form, per client IP. That form then leaves the shared limit | the shared PermitLimit
|
Yes when not a whole number above zero |
Modules:Forms:PerForm:{slug}:WindowSeconds |
Modules__Forms__PerForm__{slug}__WindowSeconds |
That form's window | the shared WindowSeconds
|
Same as above |
Modules:Forms:MaxFieldLength |
Modules__Forms__MaxFieldLength |
The longest string a submitted field may hold | 10000 |
No |
Modules:Forms:EmailVerification:CodeLifetimeMinutes |
Modules__Forms__EmailVerification__CodeLifetimeMinutes |
How long an email verification code works. Applies only to a form with verifyEmailField set |
10 |
Yes for any EmailVerification value below 1 |
Modules:Forms:EmailVerification:MaxAttempts |
Modules__Forms__EmailVerification__MaxAttempts |
Checks one code survives, right or wrong | 5 |
See above |
Modules:Forms:EmailVerification:RequestsPerClient |
Modules__Forms__EmailVerification__RequestsPerClient |
Code requests per client IP per window, across every form | 5 |
See above |
Modules:Forms:EmailVerification:RequestWindowSeconds |
Modules__Forms__EmailVerification__RequestWindowSeconds |
The window for RequestsPerClient
|
600 |
See above |
Modules:Forms:EmailVerification:CodesPerAddress |
Modules__Forms__EmailVerification__CodesPerAddress |
Codes sent to one address per window, across a tenant's forms | 5 |
See above |
Modules:Forms:EmailVerification:CodesPerForm |
Modules__Forms__EmailVerification__CodesPerForm |
Codes one form sends per window | 100 |
See above |
Modules:Forms:EmailVerification:WindowMinutes |
Modules__Forms__EmailVerification__WindowMinutes |
The window for CodesPerAddress and CodesPerForm
|
60 |
See above |
Modules:Forms:EmailVerification:SendTimeoutSeconds |
Modules__Forms__EmailVerification__SendTimeoutSeconds |
How long a code request waits for the mail provider | 10 |
See above |
Modules:Forms:Turnstile:Enabled |
Modules__Forms__Turnstile__Enabled |
Require a Cloudflare Turnstile token on submit | false |
No, but enabled with no secret refuses every submission and logs an error |
Modules:Forms:Turnstile:SecretKey secret
|
Modules__Forms__Turnstile__SecretKey |
The Turnstile secret | unset | No |
Modules:Forms:Turnstile:VerifyUrl |
Modules__Forms__Turnstile__VerifyUrl |
Turnstile's verify endpoint | https://challenges.cloudflare.com/turnstile/v0/siteverify |
No |
Modules:Pages:ContentType |
Modules__Pages__ContentType |
The content type holding the page tree | page |
No |
Modules:Pages:ParentField |
Modules__Pages__ParentField |
The reference field naming a page's parent | ParentPage |
No |
Modules:Pages:ShowInNavigationField |
Modules__Pages__ShowInNavigationField |
The boolean field that puts a page in the navigation | ShowInNavigation |
No |
Modules:Pages:OrderField |
Modules__Pages__OrderField |
The integer field that orders siblings | NavigationOrder |
No |
Modules:Pages:TitleField |
Modules__Pages__TitleField |
The field shown as a page's title | Title |
No |
Modules:Pages:MaxDepth |
Modules__Pages__MaxDepth |
The most ancestors a page may have | 8 |
No |
Modules:Pages:ReservedSlugs |
Modules__Pages__ReservedSlugs__0 |
Slugs a root page may not take | empty | No |
Modules:Pages:HomeSlug |
Modules__Pages__HomeSlug |
The root page served at /; empty means none |
home |
No |
Modules:Pages:MaxPages |
Modules__Pages__MaxPages |
The most pages one request reads | 1000 |
No |
The quickstart compose file sets Umami__Enabled and the rest of the Umami keys in their legacy root
form. Those still work and log a deprecation warning naming both sections, because
UmamiAnalyticsModule declares Umami as its legacy section. The root Umami section is
deprecated: the warning says it stops being read in a future major version, so a new deployment
should use Modules__Analytics.Umami__*. The same holds for the AI module's root Ai section and the
S3 module's Files:S3 section, the only other legacy sections a module declares.
It also sets Resend__ApiKey, and that is not a legacy form. ResendEmailModule declares no legacy
section, so there is no Modules:Email.Resend equivalent to move to, and nothing warns about it. The
core reads Resend:ApiKey at the root itself, as the Mail section above says. Leave it where it is.
Never commit these, and never log them:
JWT__Key, Secrets__Key, Mfa__Key, Connectors__Key, Metrics__ScrapeKey,
InitialAdmin__Password, the password inside ConnectionStrings__DefaultConnection or
DATABASE_URL, Resend__ApiKey, Resend__WebhookSecret, Modules__Email.Smtp__Password,
Modules__Files.S3__AccessKey, Modules__Files.S3__SecretKey,
Modules__Analytics.Umami__Password, Modules__Forms__Turnstile__SecretKey,
RateLimiting__Renderer__Key, Tracing__Otlp__Headers, and every *__ClientSecret or
*__AppSecret, which includes Oidc__Providers__{name}__ClientSecret.
CI runs Gitleaks. A hit is treated as a real incident, because rotating is the only fix once a secret is pushed.
Running
docker-compose.prod.yml,
seven variables use the ${VAR:?...} form, so compose refuses to start without them. That is the
true minimum, and the template with all seven is
.env.prod.example:
DOMAIN_API=api.example.com
[email protected]
BARAKO_TAG=4.6.0
DB_PASSWORD=<generated>
JWT_KEY=<32+ random characters>
FRONTEND_ORIGINS=https://console.example.com,https://www.example.com
ADMIN_PASSWORD=<your first sign-in>
DOMAIN_API and ACME_EMAIL are Caddy's, not the app's. The hostname has to resolve to the machine
before the first start, because Caddy asks for a certificate immediately and Let's Encrypt validates
over HTTP. BARAKO_TAG is required rather than defaulting to latest so the next
docker compose pull is not an unplanned upgrade. The other four become JWT__Key,
CORS__AllowedOrigins, InitialAdmin__Password and the password inside
ConnectionStrings__DefaultConnection.
Four more variables the file passes through carry a default equal to today's behaviour, so the stack starts without them:
| Variable | Key it sets | Default in the file | Worth setting |
|---|---|---|---|
ALLOWED_HOSTS |
AllowedHosts |
*, any host |
Yes, name the API's host |
APP_BASE_URL |
App__BaseUrl |
empty, so the feed and sitemap answer 503 | Yes |
FEEDS_SITE_URL |
Feeds__SiteUrl |
empty | Only if you publish a feed |
TRUSTED_PROXY_NETWORK |
ForwardedHeaders__KnownNetworks__0 |
172.16.0.0/12 |
Only if your Docker bridge is elsewhere |
ASPNETCORE_ENVIRONMENT=Production, ForwardedHeaders__Enabled=true and
Kubernetes__Enabled=false are written into the file, not variables, and ADMIN_USER defaults to
admin. Nothing to set for those.
TLS in front of it is not optional if browsers are involved, because of the refresh cookie. Caddy is what provides it here.
Method, so you know the shape of the gap:
-
grepacross the whole repository, excludingbin/andobj/and the test project, forGetValue,GetSection,Configuration["..."],GetConnectionString,Configure<T>(...)andBindConfiguration. - A second pass for configuration keys held in constants (
const string ...Key = "...",SectionName,Section), since several areas name their keys once and use the constant everywhere. - Every options class the second pass turned up was read for its property names and default values.
-
appsettings.json,appsettings.Development.json,quickstart/docker-compose.yml,docker-compose.hub.yml,docker-compose.yml,docker-compose.prod.ymland the three.envtemplates were read for the keys an operator actually sets. - Defaults are taken from the source, not from the compose files, where the two differ. The clearest
example is
Seed__DemoContent: the compose file pins it tofalse, the code default follows the environment.
What this will miss:
- A key built at runtime from a string that grep cannot see whole. The ones of this shape that were
found are listed (
Lifecycle:AllowSelfTransition:{name},Feeds:Paths:{type},Feeds:Titles:{type},Oidc:Providers:{name},RateLimiting:Policies:{name}andModules:Forms:PerForm:{slug}), but another could exist. - Settings that are not configuration at all:
SystemSettingdocuments edited through the admin, and anything stored in the database.Kubernetes__Enabledis the one place those two worlds meet in the core. - Settings belonging to modules this page covers only where they read configuration. Accounting,
Diagnostics, FeatureFlags, Pwa and Templates read no configuration keys of their own. Portability
reads one,
Portability:MaxImportRecords, listed under Content above. - Standard ASP.NET Core and Serilog keys that barakoCMS does not read itself.
ASPNETCORE_ENVIRONMENTis included because so much of the behaviour above hangs off it, and the Serilog level keys because they ship inappsettings.json, but the full set of framework keys is out of scope.
The three .env templates were read too, and every variable in them names a key documented above:
quickstart/.env.example,
.env.example for
docker-compose.hub.yml, and
.env.prod.example. Read them
for the shape of a working .env, and this page for what each key does.
One thing to know about the quickstart template: a variable in .env reaches the container only if
the compose file maps it to a key, and quickstart/docker-compose.yml does not map all of them.
PUBLIC_API_URL and ADMIN_PORT are documented in the file as names barakoBrew's own quickstart
expects, so they are deliberately unread here. The FILES__S3__* block is not mapped either, so
setting it in that .env does not configure S3 storage; add the keys to the api service's
environment: block yourself, in the Modules:Files.S3:* form this page gives.