Meridian handbook
ReferenceConfiguration

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:

SectionsPage
auth, mcpengine.yaml: authentication and the MCP
content, media, events, tags, approvals, endpoints, forms, search, maps, admin, notificationsengine.yaml: content, media and search
mail, newsletterengine.yaml: mail and newsletter
analytics, ai, translationengine.yaml: analytics and AI
claims, membership, portalengine.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

FormatWritten asNotes
Duration30s, 15m, 2h, 720hGo duration syntax. Units ns, us, ms, s, m, h; no days
Retention period90d, 2w, 13m, 7yWhole 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
Size25MB, 256KB, 1GBBinary units (B, KB, MB, GB, also KiB, MiB, GiB): 1MB is 1,048,576 bytes. A plain number is bytes
Rate limit60/minute/ipCount, window (second, minute, hour, day) and key
Cron schedule0 3 * * *Five fields: minute, hour, day of month, month, day of week. In UTC unless stated
Time of day03:3024-hour HH:MM
Secret${VAR}A bare interpolation, never a literal. See Configuration and secrets

engine

KeyTypeDefaultMeaning
engine.versionstring"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.environmentstringdevelopmentdevelopment, staging or production. Production enforces the rules in Configuration and secrets

dev

KeyTypeDefaultMeaning
dev.authoringbooleanfalseLets 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

KeyTypeDefaultMeaning
server.listenstring0.0.0.0:8080The host:port the public listener binds. A new project writes ${LISTEN_ADDR:-127.0.0.1:8080}
server.base_urlstringEmptyThe default site’s public origin, such as https://www.example.org, for absolute URLs, sitemaps and hreflang. Required in production
server.replicasinteger1How many engine processes run against this database. With more than one, filesystem storage must be declared shared
server.trusted_proxieslist 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_headerstringX-Forwarded-ForThe 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_timeoutduration15sThe longest the server waits to read a request
server.write_timeoutduration30sThe longest a response may take to write
server.shutdown_graceduration30sHow long in-flight requests have to finish after a stop signal
server.max_request_bytessize25MBThe largest request body. A multipart upload may be up to media.max_upload_bytes plus 1MB
server.compressionlist of strings[gzip]Response compression. Only gzip is built; br is accepted with a warning and ignored
server.admin_hoststringEmptyWhere 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_pathstring/studioThe admin’s path. Must start with /. / needs an admin_host of its own; /admin is a warning
server.portal_pathstring/membersThe member portal’s path (engine-dmo)
server.embed_pathstring/embedThe path embeddable widgets are served under
server.api.member.enabledbooleantrueServe the member API the portal and member apps use (engine-dmo)
server.api.content.enabledbooleanfalseServe the read-only content API a headless frontend reads
server.api.content.public_readsbooleanfalseAnswer a published read that carries no API key, rate limited per address. Off, every request needs a key
server.api.content.preview_ttlduration30mA headless preview token’s life, at most 2h; it also ends with the staff session it came from
server.api.content.change_retentionretention90dHow far back the change feed reaches
server.api.content.cache.s_maxageduration1mHow long a shared cache may serve a published content API response
server.api.content.cache.stale_while_revalidateduration10mHow long past s_maxage a shared cache may serve it while fetching a fresh one
server.api.content.cache.tag_headerstringCache-TagThe header cache tags travel in: Cache-Tag, Surrogate-Key, or none
server.security.signing_keysecretEmptyThe 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.enabledbooleantrueSend Strict-Transport-Security
server.security.hsts.max_ageinteger31536000Its max-age, in seconds
server.security.hsts.include_subdomainsbooleantrueAdd includeSubDomains
server.security.hsts.preloadbooleanfalseAdd preload
server.security.cspmap{}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_policystringstrict-origin-when-cross-originThe Referrer-Policy header
server.security.permissions_policystringgeolocation=(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

KeyTypeDefaultMeaning
database.urlsecretRequiredThe PostgreSQL connection string
database.max_connectionsinteger20The most connections one process holds. At least 1
database.min_connectionsinteger2Connections kept open while idle. Not above max_connections
database.max_conn_lifetimeduration1hA connection is replaced after this long
database.statement_timeoutduration30sThe session’s statement_timeout. 0s leaves the server’s

migrate

KeyTypeDefaultMeaning
migrate.on_bootstringrefuserefuse: a pending migration stops the boot. apply: every boot applies pending migrations first

schema

KeyTypeDefaultMeaning
schema.on_driftstringrefuseWhat 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_approvalstringconfig/schema/.destructive-approvedThe file, relative to the project, holding the digest of an approved destructive plan

template

KeyTypeDefaultMeaning
template.dirstringtemplatesThe template directory, relative to the project
template.contractinteger1The template contract the templates are written against. 1 is the only one this engine supports
template.strictbooleanfalseAn unknown variable path fails the page instead of rendering empty. engine-dmo check is always strict; true in production is a warning
template.cachebooleantrueKeep compiled templates. false reads every template from disk at each render, for a host whose templates are edited in place
template.error_templatestringerrors/500.htmlThe template for a server error page
template.not_found_templatestringerrors/404.htmlThe template for a not-found page

public

KeyTypeDefaultMeaning
public.mountslist[]Directories outside public/ served at a URL prefix on every site, after the site’s own public files
public.mounts[].pathstringRequiredThe URL path prefix, such as /video/
public.mounts[].dirstringRequiredThe directory, relative to the project

logging

KeyTypeDefaultMeaning
logging.levelstringinfodebug, info, warn or error
logging.formatstringjsonjson or text. Lines go to standard error
logging.redact_secretsbooleantrueAccepted. Secrets are redacted from printed configuration and stored errors whatever it says

cache

KeyTypeDefaultMeaning
cache.max_entriesinteger10000The most entries in each process’s in-memory cache (resolved lists, blocks and other derived data)
cache.page_ttlduration5mHow long a cached list or block lives. Above zero
cache.invalidation_pollduration2sHow often each process polls the database for invalidations, so a change reaches every replica
cache.pages.enabledbooleanfalseThe page cache: whole public pages for anonymous visitors, served from memory
cache.pages.freshduration1mHow long a cached page is served before it is rendered again; a publish ends it sooner
cache.pages.staleduration24hHow long past fresh a page is still served while one render replaces it
cache.pages.max_memorysize256MBThe memory the page cache may hold, per process
cache.pages.concurrencyinteger2Background renders at once. At least 1
cache.pages.warm.pathslist of strings["/"]Paths each process renders into the cache at boot, on every site under its prefix. Each starts with /
cache.pages.warm.topinteger50Also 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.deadlineduration1mThe longest the boot warm holds readiness back

storage

KeyTypeDefaultMeaning
storage.driverstringfilesystemfilesystem or s3
storage.filesystem.rootstring./storageThe directory, relative to the project or absolute. Must be writable
storage.filesystem.sharedbooleanfalseThe root is one filesystem every replica mounts. Required for server.replicas above 1 with this driver
storage.s3.endpointstringEmptyThe service’s base URL; empty for Amazon S3
storage.s3.regionstringeu-west-2Required with s3. auto for Cloudflare R2
storage.s3.bucketstringEmptyRequired with s3
storage.s3.access_key_idstringEmptyRequired with s3
storage.s3.secret_access_keysecretEmptyRequired with s3
storage.s3.path_stylebooleanfalsePath-style addressing, for MinIO and similar
storage.s3.public_base_urlstringEmptyThe https origin of a CDN in front of /media/; media addresses are written on it

Storage and media explains both drivers.

jobs

KeyTypeDefaultMeaning
jobs.workersinteger4The most jobs one process runs at once, across every queue. At least 1
jobs.queues.<queue>.workersintegerUnsetCaps one queue’s jobs at once on each process. Not negative

telemetry

KeyTypeDefaultMeaning
telemetry.metrics.enabledbooleantrueServe Prometheus metrics on a listener of their own
telemetry.metrics.bindstring127.0.0.1:9090That listener’s host:port. May not be server.listen
telemetry.metrics.pathstring/_metricsIts path. Starts with /
telemetry.metrics.exportbooleanfalseAlso push the metrics to telemetry.otlp
telemetry.metrics.intervalduration1mHow often they are pushed. At least 10s
telemetry.traces.enabledbooleanfalseExport OpenTelemetry traces to telemetry.otlp. Needs otlp.endpoint
telemetry.traces.sample_rationumber0.1The share of new traces recorded, 0 to 1
telemetry.traces.respect_parent_samplingbooleanfalseLet an inbound traceparent’s sampled flag decide
telemetry.otlp.endpointstringEmptyThe collector’s OTLP/HTTP base URL; /v1/traces and /v1/metrics are appended. No query, fragment or credentials
telemetry.otlp.headersmap of strings{}Headers sent with every export. Every value is a secret. May not set Content-Type, Content-Length or Host
telemetry.otlp.timeoutduration10sEach export’s timeout, at most 1m
telemetry.resource.service_namestringmeridianThe service.name resource attribute. Not empty
telemetry.resource.attributesmap of strings{}More resource attributes, such as deployment.environment. Not service.name
telemetry.errors.driverstringnonenone or sentry
telemetry.errors.dsnsecretEmptyThe Sentry DSN, https://<public key>@<host>/<project id>. Required with sentry
telemetry.errors.environmentstringengine.environmentThe environment reported with each event
telemetry.errors.sample_ratenumber1The share of errors reported, above 0 and at most 1. A panic is always sent

i18n

KeyTypeDefaultMeaning
i18n.require_allbooleanUnsetHow 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.

KeyTypeDefaultMeaning
retention.sessionsretention90dStaff and member sessions, after expiry or revocation
retention.login_tokensretention30dSign-in, enrolment and magic-link tokens, after expiry or use
retention.mail_eventsretention13mMail delivery events, from the event. At least mail.suppression.soft_bounce_window
retention.audit_logretention7yThe audit log, from the entry. Under a year is a warning
retention.ai_suggestionsretention13mAI 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.

KeyTypeDefaultMeaning
audit.hash_chainbooleanfalseChain 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

KeyTypeDefaultMeaning
extensionslist 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.