engine.yaml
Every key of config/engine.yaml for the server, database, migrations, schema, templates, public files, logging, caches, storage, jobs, telemetry, retention, audit and extensions, with type, default and meaning; other sections are on their own pages.
config/engine.yaml configures the install: how the engine serves, what it connects to and how it runs. It is the one configuration file every project has. A key left out takes its default, and an unknown key is an error with a suggestion (did you mean "max_connections"?).
engine:
version: "1.x"
environment: ${ENVIRONMENT:-development}
server:
listen: ${LISTEN_ADDR:-127.0.0.1:8080}
base_url: ${BASE_URL:-http://localhost:8080}
database:
url: ${DATABASE_URL} engine-dmo config render # every key with its resolved value, secrets redacted
engine-dmo config schema --file engine # the JSON Schema of this file This page covers the sections about running the engine. The rest of engine.yaml is on these pages:
| Sections | Page |
|---|---|
auth, mcp | engine.yaml: authentication and the MCP |
content, media, events, tags, approvals, endpoints, forms, search, maps, admin, notifications | engine.yaml: content, media and search |
mail, newsletter | engine.yaml: mail and newsletter |
analytics, ai, translation | engine.yaml: analytics and AI |
claims, membership, portal | engine.yaml: DMO modules |
A section that belongs to a module the binary does not compile is refused by the module’s name; claims and membership are engine-dmo only.
Value formats
| Format | Written as | Notes |
|---|---|---|
| Duration | 30s, 15m, 2h, 720h | Go duration syntax. Units ns, us, ms, s, m, h; no days |
| Retention period | 90d, 2w, 13m, 7y | Whole days, weeks, months of 30 days, or years of 365 days. Here m is months, never minutes. A Go duration such as 36h is also accepted |
| Size | 25MB, 256KB, 1GB | Binary units (B, KB, MB, GB, also KiB, MiB, GiB): 1MB is 1,048,576 bytes. A plain number is bytes |
| Rate limit | 60/minute/ip | Count, window (second, minute, hour, day) and key |
| Cron schedule | 0 3 * * * | Five fields: minute, hour, day of month, month, day of week. In UTC unless stated |
| Time of day | 03:30 | 24-hour HH:MM |
| Secret | ${VAR} | A bare interpolation, never a literal. See Configuration and secrets |
engine
| Key | Type | Default | Meaning |
|---|---|---|---|
engine.version | string | "1.x" | The engine major version the project targets, as <major>.x. A release binary from 1.0.0 on refuses a project whose major differs from its own |
engine.environment | string | development | development, staging or production. Production enforces the rules in Configuration and secrets |
dev
| Key | Type | Default | Meaning |
|---|---|---|---|
dev.authoring | boolean | false | Lets the authoring commands (engine-dmo dev field add and the rest, and engine-dmo mcp over standard input and output) run in staging. They always run in development and never in production |
server
| Key | Type | Default | Meaning |
|---|---|---|---|
server.listen | string | 0.0.0.0:8080 | The host:port the public listener binds. A new project writes ${LISTEN_ADDR:-127.0.0.1:8080} |
server.base_url | string | Empty | The default site’s public origin, such as https://www.example.org, for absolute URLs, sitemaps and hreflang. Required in production |
server.replicas | integer | 1 | How many engine processes run against this database. With more than one, filesystem storage must be declared shared |
server.trusted_proxies | list of strings | [] | IP addresses or CIDR ranges of the proxies and load balancers in front of the engine. Forwarded headers are believed only from these |
server.real_ip_header | string | X-Forwarded-For | The header a trusted proxy puts the client address in. In production, a non-empty value with no trusted_proxies is an error; set "" when nothing is in front |
server.read_timeout | duration | 15s | The longest the server waits to read a request |
server.write_timeout | duration | 30s | The longest a response may take to write |
server.shutdown_grace | duration | 30s | How long in-flight requests have to finish after a stop signal |
server.max_request_bytes | size | 25MB | The largest request body. A multipart upload may be up to media.max_upload_bytes plus 1MB |
server.compression | list of strings | [gzip] | Response compression. Only gzip is built; br is accepted with a warning and ignored |
server.admin_host | string | Empty | Where the admin lives: empty puts it on the default site’s host; * on every site’s host (a session per host); a host name such as studio.example.org on a host of its own (no scheme, no path) |
server.admin_path | string | /studio | The admin’s path. Must start with /. / needs an admin_host of its own; /admin is a warning |
server.portal_path | string | /members | The member portal’s path (engine-dmo) |
server.embed_path | string | /embed | The path embeddable widgets are served under |
server.api.member.enabled | boolean | true | Serve the member API the portal and member apps use (engine-dmo) |
server.api.content.enabled | boolean | false | Serve the read-only content API a headless frontend reads |
server.api.content.public_reads | boolean | false | Answer a published read that carries no API key, rate limited per address. Off, every request needs a key |
server.api.content.preview_ttl | duration | 30m | A headless preview token’s life, at most 2h; it also ends with the staff session it came from |
server.api.content.change_retention | retention | 90d | How far back the change feed reaches |
server.api.content.cache.s_maxage | duration | 1m | How long a shared cache may serve a published content API response |
server.api.content.cache.stale_while_revalidate | duration | 10m | How long past s_maxage a shared cache may serve it while fetching a fresh one |
server.api.content.cache.tag_header | string | Cache-Tag | The header cache tags travel in: Cache-Tag, Surrogate-Key, or none |
server.security.signing_key | secret | Empty | The install’s master key: CSRF tokens, encryption of staff second-factor secrets at rest, hashed rate-limit keys and derived keys. At least 32 characters. Required in production; when it is empty outside production, a key is derived from the database URL, with a warning. Once the file names ${SIGNING_KEY}, the variable must be set (empty is allowed outside production) for every command that loads the configuration. See Setting them in development |
server.security.hsts.enabled | boolean | true | Send Strict-Transport-Security |
server.security.hsts.max_age | integer | 31536000 | Its max-age, in seconds |
server.security.hsts.include_subdomains | boolean | true | Add includeSubDomains |
server.security.hsts.preload | boolean | false | Add preload |
server.security.csp | map | {} | The public sites’ Content Security Policy: each key a directive with underscores for hyphens (script_src), each value a list of sources (or one string). Empty sources are dropped. 'unsafe-inline' and 'unsafe-eval' are refused in script_src and default_src. Empty sends no policy on public pages; the admin has its own fixed policy |
server.security.referrer_policy | string | strict-origin-when-cross-origin | The Referrer-Policy header |
server.security.permissions_policy | string | geolocation=(self), camera=(), microphone=() | The Permissions-Policy header |
server:
security:
csp:
default_src: ["'self'"]
script_src: ["'self'"]
style_src: ["'self'"]
style_src_attr: ["'unsafe-inline'"] # the dominant-colour image placeholders
img_src: ["'self'", "data:"]
connect_src: ["'self'"]
object_src: ["'none'"]
frame_ancestors: ["'self'"] database
| Key | Type | Default | Meaning |
|---|---|---|---|
database.url | secret | Required | The PostgreSQL connection string |
database.max_connections | integer | 20 | The most connections one process holds. At least 1 |
database.min_connections | integer | 2 | Connections kept open while idle. Not above max_connections |
database.max_conn_lifetime | duration | 1h | A connection is replaced after this long |
database.statement_timeout | duration | 30s | The session’s statement_timeout. 0s leaves the server’s |
migrate
| Key | Type | Default | Meaning |
|---|---|---|---|
migrate.on_boot | string | refuse | refuse: a pending migration stops the boot. apply: every boot applies pending migrations first |
schema
| Key | Type | Default | Meaning |
|---|---|---|---|
schema.on_drift | string | refuse | What the boot does when the configuration differs from the database: refuse, apply (approved destructive plans only) or warn (apply what is safe, start anyway). warn is refused in production |
schema.destructive_approval | string | config/schema/.destructive-approved | The file, relative to the project, holding the digest of an approved destructive plan |
template
| Key | Type | Default | Meaning |
|---|---|---|---|
template.dir | string | templates | The template directory, relative to the project |
template.contract | integer | 1 | The template contract the templates are written against. 1 is the only one this engine supports |
template.strict | boolean | false | An unknown variable path fails the page instead of rendering empty. engine-dmo check is always strict; true in production is a warning |
template.cache | boolean | true | Keep compiled templates. false reads every template from disk at each render, for a host whose templates are edited in place |
template.error_template | string | errors/500.html | The template for a server error page |
template.not_found_template | string | errors/404.html | The template for a not-found page |
public
| Key | Type | Default | Meaning |
|---|---|---|---|
public.mounts | list | [] | Directories outside public/ served at a URL prefix on every site, after the site’s own public files |
public.mounts[].path | string | Required | The URL path prefix, such as /video/ |
public.mounts[].dir | string | Required | The directory, relative to the project |
logging
| Key | Type | Default | Meaning |
|---|---|---|---|
logging.level | string | info | debug, info, warn or error |
logging.format | string | json | json or text. Lines go to standard error |
logging.redact_secrets | boolean | true | Accepted. Secrets are redacted from printed configuration and stored errors whatever it says |
cache
| Key | Type | Default | Meaning |
|---|---|---|---|
cache.max_entries | integer | 10000 | The most entries in each process’s in-memory cache (resolved lists, blocks and other derived data) |
cache.page_ttl | duration | 5m | How long a cached list or block lives. Above zero |
cache.invalidation_poll | duration | 2s | How often each process polls the database for invalidations, so a change reaches every replica |
cache.pages.enabled | boolean | false | The page cache: whole public pages for anonymous visitors, served from memory |
cache.pages.fresh | duration | 1m | How long a cached page is served before it is rendered again; a publish ends it sooner |
cache.pages.stale | duration | 24h | How long past fresh a page is still served while one render replaces it |
cache.pages.max_memory | size | 256MB | The memory the page cache may hold, per process |
cache.pages.concurrency | integer | 2 | Background renders at once. At least 1 |
cache.pages.warm.paths | list of strings | ["/"] | Paths each process renders into the cache at boot, on every site under its prefix. Each starts with / |
cache.pages.warm.top | integer | 50 | Also warm each site’s most visited pages of the last 30 days (the sitemap’s first pages when there are no analytics) |
cache.pages.warm.deadline | duration | 1m | The longest the boot warm holds readiness back |
storage
| Key | Type | Default | Meaning |
|---|---|---|---|
storage.driver | string | filesystem | filesystem or s3 |
storage.filesystem.root | string | ./storage | The directory, relative to the project or absolute. Must be writable |
storage.filesystem.shared | boolean | false | The root is one filesystem every replica mounts. Required for server.replicas above 1 with this driver |
storage.s3.endpoint | string | Empty | The service’s base URL; empty for Amazon S3 |
storage.s3.region | string | eu-west-2 | Required with s3. auto for Cloudflare R2 |
storage.s3.bucket | string | Empty | Required with s3 |
storage.s3.access_key_id | string | Empty | Required with s3 |
storage.s3.secret_access_key | secret | Empty | Required with s3 |
storage.s3.path_style | boolean | false | Path-style addressing, for MinIO and similar |
storage.s3.public_base_url | string | Empty | The https origin of a CDN in front of /media/; media addresses are written on it |
Storage and media explains both drivers.
jobs
| Key | Type | Default | Meaning |
|---|---|---|---|
jobs.workers | integer | 4 | The most jobs one process runs at once, across every queue. At least 1 |
jobs.queues.<queue>.workers | integer | Unset | Caps one queue’s jobs at once on each process. Not negative |
telemetry
| Key | Type | Default | Meaning |
|---|---|---|---|
telemetry.metrics.enabled | boolean | true | Serve Prometheus metrics on a listener of their own |
telemetry.metrics.bind | string | 127.0.0.1:9090 | That listener’s host:port. May not be server.listen |
telemetry.metrics.path | string | /_metrics | Its path. Starts with / |
telemetry.metrics.export | boolean | false | Also push the metrics to telemetry.otlp |
telemetry.metrics.interval | duration | 1m | How often they are pushed. At least 10s |
telemetry.traces.enabled | boolean | false | Export OpenTelemetry traces to telemetry.otlp. Needs otlp.endpoint |
telemetry.traces.sample_ratio | number | 0.1 | The share of new traces recorded, 0 to 1 |
telemetry.traces.respect_parent_sampling | boolean | false | Let an inbound traceparent’s sampled flag decide |
telemetry.otlp.endpoint | string | Empty | The collector’s OTLP/HTTP base URL; /v1/traces and /v1/metrics are appended. No query, fragment or credentials |
telemetry.otlp.headers | map of strings | {} | Headers sent with every export. Every value is a secret. May not set Content-Type, Content-Length or Host |
telemetry.otlp.timeout | duration | 10s | Each export’s timeout, at most 1m |
telemetry.resource.service_name | string | meridian | The service.name resource attribute. Not empty |
telemetry.resource.attributes | map of strings | {} | More resource attributes, such as deployment.environment. Not service.name |
telemetry.errors.driver | string | none | none or sentry |
telemetry.errors.dsn | secret | Empty | The Sentry DSN, https://<public key>@<host>/<project id>. Required with sentry |
telemetry.errors.environment | string | engine.environment | The environment reported with each event |
telemetry.errors.sample_rate | number | 1 | The share of errors reported, above 0 and at most 1. A panic is always sent |
i18n
| Key | Type | Default | Meaning |
|---|---|---|---|
i18n.require_all | boolean | Unset | How strictly engine-dmo check holds schema labels to the enabled locales. Unset: a label missing in the source locale is an error, in any other enabled locale a warning. true: missing in any enabled locale is an error. false: only the source locale is required |
retention
How long rows that carry personal data are kept after they stop being used. Each is a retention period of at least 1d.
| Key | Type | Default | Meaning |
|---|---|---|---|
retention.sessions | retention | 90d | Staff and member sessions, after expiry or revocation |
retention.login_tokens | retention | 30d | Sign-in, enrolment and magic-link tokens, after expiry or use |
retention.mail_events | retention | 13m | Mail delivery events, from the event. At least mail.suppression.soft_bounce_window |
retention.audit_log | retention | 7y | The audit log, from the entry. Under a year is a warning |
retention.ai_suggestions | retention | 13m | AI suggestions once decided, failed or answered |
audit
The audit log’s hash chain. Turn it on to make changes to the log detectable with engine-dmo audit verify.
| Key | Type | Default | Meaning |
|---|---|---|---|
audit.hash_chain | boolean | false | Chain every audit row to the one before it in its realm, under a per-realm lock that serialises the realm’s writes. With it on, a daily audit.anchor task logs each realm’s head |
extensions
| Key | Type | Default | Meaning |
|---|---|---|---|
extensions | list of strings | [] | The extensions the engine loads at boot. A bare name (vite) is a first-party extension shipped beside the binary (/app/extensions/vite.so in the -vips image); a path, relative to the project, is your own .so. Only cgo builds load extensions; the pure Go image refuses with a message |
See Extending for building one.
Configuration
Every configuration file a Meridian project can have, where it lives, what it holds and which page lists its keys, plus how files are loaded, checked, given ids and validated in an editor.
engine.yaml: authentication and the MCP
Every key of engine.yaml's auth section (local staff accounts, OpenID Connect single sign-on, staff and member sessions, member sign-in) and its mcp section (the remote content MCP endpoint, its clients, tokens and limits), with type, default and meaning.