Skip to content

Configuration

Arnel Robles edited this page Sep 11, 2026 · 1 revision

Configuration

Every setting barakoCMS reads, generated from the 4.0.1 source on master. 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.

Read this first

Three defaults account for most first-deployment surprises. All three are verifiable in the source and each is covered in its own section below.

  1. JWT__Key must be 32 characters or more, or the process exits at startup. A value starting with REPLACE_THIS is also refused, because the shipped k8s secret carries a length-valid placeholder.
  2. Seed__DemoContent is 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.
  3. The refresh cookie is marked Secure outside Development. A Production API served over plain HTTP cannot keep a browser session, because the browser will not send the cookie back.

How configuration is read

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).

Database

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.

Auth and tokens

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 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

The 32 character floor

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.

The refresh cookie and plain HTTP

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();

(RefreshTokenCookie.cs)

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.

Secrets and encryption keys

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).

CORS, hosts and proxy

Key Environment What it does Default Refuses to start without it
CORS:AllowedOrigins CORS__AllowedOrigins Comma-separated browser origins allowed with credentials http://localhost:3000,http://localhost:3001,https://localhost:7049; an empty value falls back to those same three 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

CORS:AllowedOrigins also governs which response headers scripts may read: ETag and X-Api-Contract-Version are exposed on both the configured and the fallback branch.

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.

Seeding

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

The InitialAdmin keys are first-boot only

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.

What Seed__DemoContent actually seeds

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 with FirstName, LastName, Email, BirthDay, JobDescription, Gender and SSN, where SSN is marked Sensitive and masked to its last four characters.
  • Three sample records, deliberately unusable as personal data (Sample Employee One and friends, mail at example.com, SSN values that read SAMPLE-NOT-A-REAL-SSN-1).
  • A workflow named Attendance Confirmation Email, stored active, triggering on AttendanceRecord Created with condition status = Published, whose single action is SendEmail to {{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.

Modules

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.

Mail

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.

Storage and files

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

Upload scanning needs both scanner keys

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. NoFileScanner is registered and its Configured is false.
  • Scoped key only. ClamAvScanner is registered, finds no address at the root, and its Configured is 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.

Content and public delivery

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 Sensitivity enforcement mode; an unparseable value falls back to the default SensitiveOnly No
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
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

Workflows, webhooks and jobs

Key Environment What it does Default Refuses to start without it
Workflows:RunnerEnabled Workflows__RunnerEnabled The background workflow runner true No
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

Observability and the API explorer

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

/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.

Other modules

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
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

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.

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.

Secrets checklist

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, and every *__ClientSecret or *__AppSecret.

CI runs Gitleaks. A hit is treated as a real incident, because rotating is the only fix once a secret is pushed.

The smallest working production set

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
ACME_EMAIL=you@example.com
BARAKO_TAG=4.0.1
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.

How these keys were enumerated, and what might be missing

Method, so you know the shape of the gap:

  1. grep across the whole repository, excluding bin/ and obj/ and the test project, for GetValue, GetSection, Configuration["..."], GetConnectionString, Configure<T>(...) and BindConfiguration.
  2. 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.
  3. Every options class the second pass turned up was read for its property names and default values.
  4. appsettings.json, appsettings.Development.json, quickstart/docker-compose.yml, docker-compose.hub.yml, docker-compose.yml, docker-compose.prod.yml and the three .env templates were read for the keys an operator actually sets.
  5. 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 to false, 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} and Feeds:Titles:{type}), but another could exist.
  • Settings that are not configuration at all: SystemSetting documents edited through the admin, and anything stored in the database. Kubernetes__Enabled is 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, Portability, Pwa and Templates read no configuration keys of their own.
  • Standard ASP.NET Core and Serilog keys that barakoCMS does not read itself. ASPNETCORE_ENVIRONMENT is included because so much of the behaviour above hangs off it, and the Serilog level keys because they ship in appsettings.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.

Clone this wiki locally