Skip to main content

app-config.yaml structure

The normative specification does not pin down which sections must exist — every top-level section is defined by the consuming application. Below are the conventions for naming and structuring the file that have emerged across the dagstack ecosystem.

Common top-level sections

A set of sections that show up in most dagstack applications regardless of domain (billing, analytics, chatbot, indexer, notification service, and so on):

SectionPurposeRequired
appApplication metadata (name, tagline).No, but recommended.
dagstackplugin-system settings (plugin_dirs, overrides).If you use plugin-system.
loggingStructured logger (dagstack/logger-spec).Recommended.
tenancyMulti-tenancy model (dagstack/tenancy-spec).If multi-tenant.
databaseDatabase connection (host, port, pool_size).Domain-specific.
cacheRedis / memcached / local cache.Domain-specific.
metricsPrometheus / OTLP endpoint.Recommended in production.
<kind>.<name>The section of a specific plugin of kind kind named name.plugin-system apps.

Everything else is a domain-specific section dictated by the application (billing, notifications, workers, auth, rag, email, scheduler, api, and so on).

Naming conventions

RuleExample
snake_case for keysbase_url, max_retries
Singular noundatabase, not databases
Booleans without is_ / has_enabled, not is_enabled
Timestamps in ISO 8601"2026-04-21T12:00:00Z"
Durations with _s / _ms / _min suffixtimeout_s: 30, cache_ttl_min: 15
Sizes with _bytes / _mb / _gb suffixmax_upload_mb: 10

Example: service backend

app-config.yaml
app:
name: "order-service"
tagline: "Order processor"

dagstack:
plugin_dirs:
- plugins/

database:
host: "${DB_HOST:-localhost}"
port: "${DB_PORT:-5432}"
name: "${DB_NAME:-orders}"
user: "${DB_USER}"
password: "${DB_PASSWORD}"
pool_size: 20

cache:
url: "${REDIS_URL:-redis://localhost:6379/0}"
ttl_min: 15

api:
host: "0.0.0.0"
port: 8080
cors_origins:
- "https://app.example.com"

workers:
concurrency: 8
queue_name: "orders"

logging:
level: "${LOG_LEVEL:-INFO}"
format: "${LOG_FORMAT:-json}"

tenancy:
mode: "${TENANCY_MODE:-single}"

Example: data-processing application with plugin-system

app-config.yaml
app:
name: "data-indexer"

dagstack:
plugin_dirs:
- plugins/

# Sections of specific plugins follow the rule <kind>.<name>:

source:
s3_bucket: # section for the source.s3_bucket plugin
bucket: "${INGEST_BUCKET}"
region: "eu-central-1"
postgres: # section for the source.postgres plugin
url: "${SOURCE_DB_URL}"

processor:
normalize: # section for the processor.normalize plugin
locale: "en_US"
enrich: # section for the processor.enrich plugin
api_key: "${ENRICHMENT_API_KEY}"

sink:
elasticsearch: # section for the sink.elasticsearch plugin
url: "${ES_URL}"
index: "documents"

Each plugin reads its own <kind>.<name> section through config.getSection() — a shared pattern regardless of the application's domain.

Domain-specific examples

dagstack works equally well for different classes of applications — a few typical configs:

E-commerce billing (payment processing):

billing:
provider: "${BILLING_PROVIDER:-stripe}"
webhook_secret: "${BILLING_WEBHOOK_SECRET}"
currency: "USD"
tax_rates:
default: 0.20
reduced: 0.05

Notification aggregator:

notifications:
email:
smtp_host: "${SMTP_HOST}"
from_address: "no-reply@example.com"
sms:
api_key: "${SMS_API_KEY}"
from_number: "+1-555-0100"
push:
fcm_key: "${FCM_KEY}"

AI / RAG platform (one of many possible shapes):

llm:
base_url: "${OPENAI_BASE_URL:-https://api.openai.com/v1}"
api_key: "${OPENAI_API_KEY}"
model: "${OPENAI_MODEL:-gpt-4o-mini}"

vector_store:
url: "${QDRANT_URL:-http://localhost:6333}"
api_key: "${QDRANT_API_KEY}"

retrieval:
top_k: 10
min_score: 0.55

In all three cases the config-stack mechanics are the same — layers, env interpolation, getSection(), change subscriptions. Domain-specific knowledge lives in the application, not in the config stack.

Merge rules for arrays

app-config.yaml
dagstack:
plugin_dirs:
- plugins/
- examples/plugins/
app-config.production.yaml
dagstack:
plugin_dirs:
- /opt/dagstack/plugins/

After the merge (DAGSTACK_ENV=production):

dagstack:
plugin_dirs:
- /opt/dagstack/plugins/ # the array is fully replaced

The array is not concatenated. See Layers for the merge rules.

Null in an override layer

app-config.yaml
cache:
redis:
url: "redis://localhost:6379/0"
app-config.local.yaml
cache:
redis: null # explicitly disable the cache locally

null in an override layer explicitly overrides the base value. Omitting the key from the override leaves the base alone.

Comments

YAML supports # comments. A good practice:

api:
# Maximum requests per second per client. Default is 10; for internal
# services you can raise it up to 100.
rate_limit_rps: 10

Comments help operators understand the purpose of non-obvious fields.

See also