Skip to main content

Configuration Reference

All configuration is managed via appsettings.json and can be overridden with environment variables using the __ (double-underscore) convention.

Connection Strings​

{
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=HeadlessEcommerce;Username=postgres;Password=postgres123"
}
}
KeyDescription
DefaultConnectionPostgreSQL connection string

JWT Authentication​

{
"Jwt": {
"SecretKey": "YourSuperSecretKeyHere12345678901234567890",
"Issuer": "HeadlessEcommerceApi",
"Audience": "HeadlessEcommerceClient",
"ExpirationMinutes": 60
}
}
KeyDescriptionDefault
SecretKeyHMAC signing key (min 32 chars)—
IssuerToken issuer claimHeadlessEcommerceApi
AudienceToken audience claimHeadlessEcommerceClient
ExpirationMinutesAccess token lifetime60
danger

Use a strong, random SecretKey in production. Never reuse the development key.

RabbitMQ​

{
"RabbitMQ": {
"Host": "localhost",
"Port": 5672,
"VirtualHost": "/",
"Username": "guest",
"Password": "guest",
"QueuePrefix": "quark",
"EnableOutbox": true,
"OutboxDeliveryIntervalSeconds": 5,
"MaxRetryCount": 5,
"RetryIntervalSeconds": 5
}
}
KeyDescriptionDefault
HostRabbitMQ server hostnamelocalhost
PortAMQP port5672
VirtualHostRabbitMQ virtual host/
QueuePrefixPrefix for queue namesquark
EnableOutboxEnable transactional outboxtrue
OutboxDeliveryIntervalSecondsPolling interval for outbox publisher5
MaxRetryCountMax delivery attempts before marking failed5

The outbox publisher writes events to the database in the same transaction as business operations, then a background service polls and publishes them to RabbitMQ via a topic exchange (quark.events).

Storage​

{
"Storage": {
"Provider": "Local",
"BaseUrl": "http://localhost:8080/media",
"MaxFileSizeMB": 50,
"AllowedExtensions": [".jpg", ".png", ".webp", "..."],
"Local": {
"RootPath": "wwwroot/media",
"RequestPath": "/media"
},
"NetworkShare": {
"RootPath": "//nas01/commerce-media",
"BaseUrl": "https://cdn.example.com"
},
"S3": {
"Bucket": "my-commerce-assets",
"Region": "eu-central-1",
"BaseUrl": "https://cdn.example.com",
"UsePresignedUrls": false
}
}
}
KeyDescriptionDefault
ProviderStorage backend: Local, NetworkShare, or S3Local
BaseUrlPublic base URL for media fileshttp://localhost:8080/media
MaxFileSizeMBMaximum upload file size50
tip

In production, set Storage.BaseUrl to your public domain (e.g., https://quarkapi.yourdomain.com/media). This URL is stored in the database and used to construct media URLs returned by the API.

Rate Limiting​

{
"RateLimiting": {
"Enabled": false,
"Fixed": { "PermitLimit": 100, "WindowSeconds": 60, "QueueLimit": 0 },
"Search": { "PermitLimit": 60, "WindowSeconds": 60, "QueueLimit": 0 },
"Sliding": { "PermitLimit": 20, "WindowSeconds": 10, "SegmentsPerWindow": 2, "QueueLimit": 0 },
"ChannelRedirect": { "PermitLimit": 60, "WindowSeconds": 60, "QueueLimit": 0 }
}
}

Rate limiting is off by default. A new installation limits nothing until you set RateLimiting:Enabled to true (environment variable RateLimiting__Enabled=true); the API logs which state it started in. Turn it on once the installation is reachable through its real proxy chain and you have confirmed visitor addresses arrive — see Reverse Proxies and Client Addresses. Sign-in codes and password attempts have their own per-account throttling, which is always active and does not depend on this switch.

Every limit is per client, never for the API as a whole. A signed-in user is counted by user id; anyone else by address (an IPv6 visitor by their /64). One visitor running out of requests has no effect on any other.

PolicyApplied toDefault
SearchProduct search60 per minute
SlidingSession refresh and one-time-code sign-in20 per 10 seconds
FixedSubmitting a review100 per minute
ChannelRedirectChannel short-link redirects60 per minute

Endpoints without a policy are not rate limited. QueueLimit is how many over-limit requests wait for the next window instead of being rejected; leave it at 0 so a client is told at once (429 with Retry-After) rather than holding a connection open.

Reverse Proxies and Client Addresses​

Per-client limits are only as good as the address the API sees. Behind a reverse proxy, load balancer or ingress every connection arrives from the proxy's address, so the API reads the visitor's address from X-Forwarded-For — but only from proxies it trusts. The same address is what payment providers, consent records and sign-in throttling receive.

{
"ForwardedHeaders": {
"Enabled": true,
"TrustPrivateNetworks": true,
"KnownProxies": [],
"KnownNetworks": [],
"ForwardLimit": null
}
}
SettingDescriptionDefault
EnabledRead X-Forwarded-For and X-Forwarded-Proto at alltrue
TrustPrivateNetworksTrust peers on loopback, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and fc00::/7 as proxiestrue
KnownProxiesFurther proxy addresses to trustnone
KnownNetworksFurther proxy networks to trust, in CIDR form — for example a CDN's published rangesnone
ForwardLimitMost hops to accept; null means as many as are trustednull

The header is read from right to left, and a hop is accepted only while the address that reported it is trusted. The result is the first address outside your own infrastructure. Anything a client writes into the header itself sits further left and is never reached, so it cannot be used to dodge a limit or impersonate someone else. If nothing is trusted, the headers are ignored entirely.

The defaults suit Docker, Coolify and Kubernetes, where the proxy's address is private and changes between deployments. Two things to check:

  • Every proxy in the chain must pass the address on. If you run two (an edge proxy in front of the platform's own), the inner one must be told to trust the outer — in Traefik, forwardedHeaders.trustedIPs on the entry point; in NGINX, set_real_ip_from / real_ip_recursive. Otherwise it overwrites the header with the outer proxy's address.
  • A CDN in front (Cloudflare and the like) connects from public addresses: add its ranges to KnownNetworks.

If the API still ends up counting visitors against a private address, it logs a warning the first time it happens: "Rate limiting is counting anonymous requests against the private address …". Until that is fixed, all anonymous visitors behind that proxy share one limit.

caution

Set TrustPrivateNetworks to false only if the API is reachable directly from an internal network whose users you do not want to be able to set their own X-Forwarded-For. Then list your proxies explicitly in KnownProxies.

Checkout​

{
"FrontendUrl": "https://shop.example.com",
"Checkout": {
"AllowedReturnUrls": [ "myapp://", "https://second-shop.example.com/" ]
}
}
SettingDescription
FrontendUrlWhere shoppers are sent after an off-site payment step when the store has no Url of its own
Checkout:AllowedReturnUrlsPrefixes a client may name as returnUrl when it starts a payment — a mobile app's scheme, a further storefront domain. Anything under the store's own Url is always allowed; everything else is refused, because the API redirects to it

As environment variables: Checkout__AllowedReturnUrls__0=myapp://, Checkout__AllowedReturnUrls__1=…. See Payments.

Payment Reconciliation​

Settles payments whose result never reached the store (the shopper closed the tab, a lost browser return, a late bank answer) by asking the gateway. On by default.

{
"Payments": {
"Reconciliation": {
"Enabled": true,
"IntervalSeconds": 300,
"MinAgeMinutes": 10,
"MaxAgeHours": 24,
"BatchSize": 50
}
}
}
SettingDescription
EnabledTurn the background check off (e.g. on a second API replica; running it on several is safe but redundant)
IntervalSecondsHow often it runs (minimum 30)
MinAgeMinutesPayments younger than this are left to the shopper's own return
MaxAgeHoursAfter this, a payment with no trace at the gateway is closed as abandoned; one the gateway knows of but never decided is flagged requires review
BatchSizePayments checked per run. Each is re-checked with back-off: a quarter of its age, between 5 minutes and 6 hours

As environment variables: Payments__Reconciliation__Enabled=false, …

Licensing​

{
"Licensing": {
"PublicKeyBase64": "base64-encoded-rsa-4096-public-key",
"LicenseFilePath": "/path/to/license.lic",
"LicenseFileBase64": "base64-encoded-license-file",
"PhoneHomeUrl": "https://license-server.yourdomain.com/api/phone-home"
}
}
KeyDescription
PublicKeyBase64RSA-4096 public key for signature verification
LicenseFilePathPath to license file on disk (local dev)
LicenseFileBase64Base64-encoded license file (production, via env var)
PhoneHomeUrlOptional URL for periodic license validation callbacks

For production, prefer environment variables:

Licensing__PublicKeyBase64=...
Licensing__LicenseFileBase64=...

Stripe​

{
"Stripe": {
"SecretKey": "sk_test_..."
}
}

Caching​

{
"Caching": {
"Enabled": true
}
}

When enabled, storefront endpoints use response caching (300s default) with Vary on store/language/currency headers.

Logging​

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

Standard ASP.NET Core logging configuration. Adjust log levels per namespace as needed.

Environment Variable Mapping​

The __ convention maps to JSON nesting:

Environment VariableJSON Path
ConnectionStrings__DefaultConnectionConnectionStrings.DefaultConnection
Jwt__SecretKeyJwt.SecretKey
RabbitMQ__HostRabbitMQ.Host
Storage__BaseUrlStorage.BaseUrl
Licensing__PublicKeyBase64Licensing.PublicKeyBase64