API Reference
API Reference
GitHub
Getting Started

Configuration

Environment variables and configuration options

Mailyard reads ./mailyard.yaml (override with --config) and every key can also be set as an environment variable. The variable name is the YAML path, uppercased, with dots replaced by underscores and a MAILYARD_ prefix — server.public_url is MAILYARD_SERVER_PUBLIC_URL. Environment wins over the file, and the file is optional, so a deployment can be configured entirely from the environment.

Put secrets in the environment rather than the file: MAILYARD_AUTH_JWT_SECRET, MAILYARD_DATABASE_CRYPTO_ENCRYPTION_KEY, MAILYARD_DATABASE_DSN and MAILYARD_RELAY_NODES_AUTO_REGISTER_TOKEN.

Server

VariableDefaultDescription
MAILYARD_SERVER_ADDR:3000Bind address for the HTTP console and API
MAILYARD_SERVER_PUBLIC_URLPublic base URL, e.g. https://mail.example.com. Required for anything that has to build an absolute link: invitations, password resets, OIDC redirects, tracking links and hosted unsubscribe pages. It also decides the Secure flag on the session cookie
MAILYARD_SERVER_TRUSTED_PROXIESProxy IPs or CIDRs whose X-Forwarded-For is believed. Empty means the direct peer is the client, which is correct for direct exposure. Set it to your load balancer’s CIDRs when TLS terminates upstream, otherwise the rate limiter, audit log and access log all record the proxy’s address. List every hop, not just the nearest one — see Trusting a proxy
MAILYARD_SERVER_MAX_CONCURRENT_REQUESTS4096Requests served at once. A request body is read into memory before any handler runs, so this is what bounds how many large uploads can exist at the same time. 0 removes the cap
MAILYARD_SERVER_TLS_ENABLEDfalseTerminate TLS here rather than at a proxy in front — see TLS

public_url must match the scheme and host you actually browse

Two ways to get locked out of a console that is working perfectly, neither of which reports anything:

  • Scheme. https://… sets Secure on the session cookie. Open the console over plain http:// and the login returns 200, the browser discards the cookie, and every request after it is unauthenticated.
  • A bare IP address. Safari will not keep a cookie for a host like http://192.168.1.10:3000 — an IP literal has no registrable domain and WebKit needs one. Chrome and Firefox are fine with it. Give the installation a hostname instead.

Both land on the login page with authentication required.

The console frontend and the documentation you are reading are compiled into the binary, so there is no asset path to configure.

Database (PostgreSQL)

PostgreSQL is the only supported engine. One connection string, no host/port/user parts - put it in MAILYARD_DATABASE_DSN rather than YAML so the password stays out of the config file.

VariableDefaultDescription
MAILYARD_DATABASE_DSNRequired. postgres://user:pass@host:5432/mailyard?sslmode=require
MAILYARD_DATABASE_REPLICA_DSNSRead-only followers, comma separated. See Read replicas
database:
    dsn: postgres://mailyard:secret@localhost:5432/mailyard?sslmode=disable

Migrations run automatically at startup (goose, embedded in the binary), so a fresh empty database is all the service needs.

PostgreSQL is the only service to configure: it holds the delivery queue and is how several nodes coordinate - see Scaling out .

Logging

One sink, written exactly where you point it.

VariableDefaultDescription
MAILYARD_LOGGING_LEVELinfotrace, debug, info, warn or error
MAILYARD_LOGGING_FORMATjsonjson for shippers, text for reading
MAILYARD_LOGGING_OUTPUTstdoutstdout, stderr, or a file path (opened for append, created when missing)
MAILYARD_LOGGING_COLORtrueANSI colors, text format only. Honored only when the output is an interactive terminal, so a file or a piped stdout stays plain.

An invalid format or level fails at startup rather than falling back, because a silent fallback to human-readable text produces a log your shipper cannot parse and never says why.

The first-run bootstrap password is written straight to stderr, not through the logger, so it is still visible when logs are directed to a file.

Authentication

VariableDefaultDescription
MAILYARD_AUTH_JWT_SECRETRequired. Signs session JWTs. Generate with openssl rand -hex 32. Every other HMAC in the system (OIDC state cookie, tracking signer) uses a separate subkey derived from it, never the raw value
MAILYARD_AUTH_LOCAL_ENABLEDfalseEmail and password sign-in
MAILYARD_AUTH_LOCAL_EMAILBootstrap admin address. On first start against an empty users table this account is created with a generated password, printed to stderr once
MAILYARD_AUTH_SESSION_TTL12hSession cookie lifetime, as a Go duration
MAILYARD_AUTH_REGISTRATION_ENABLEDfalseOpens POST /app/api/auth/register for public self-signup. Off by default: this is an operator console, and an open signup on an internet-facing install means strangers with accounts. New accounts are always plain users — an admin grants roles afterwards
MAILYARD_AUTH_DISABLEDfalseRemoves the authentication gate from every API surface. Local development only

Danger

MAILYARD_AUTH_DISABLED=true makes every console endpoint reachable without credentials, including the platform-admin routes. It exists so a developer can poke at the API without logging in. Never set it on anything reachable from a network you do not control.

Single sign-on is configured per provider in the console under Admin → OAuth Providers, not through the environment, because the settings are stored in the database and editable at runtime. See OAuth Providers .

Encryption at rest

VariableDefaultDescription
MAILYARD_DATABASE_CRYPTO_ENCRYPTION_KEYrequiredKeys the at-rest encryption of secrets stored in the database: tenant SMTP passwords, DKIM private keys, TOTP secrets, OAuth client secrets, certificate private keys. At least 32 characters - generate with openssl rand -hex 32. The AES-256 key is derived with HKDF-SHA256, which stretches a short secret without adding entropy to it

Required. Mailyard refuses to start without it. It used to fall back to base64 encoding with a warning at startup, which meant an install that missed the warning kept every one of those secrets in a reversible encoding, in columns documented as encrypted. Refusing to boot is the honest version of that warning.

Changing it later orphans every row already sealed under the old key, so treat it like a database credential.

A stored value is base64(nonce||ciphertext) and nothing else. Older releases wrote an enc: prefix in front of it to mark sealed values apart from the base64 fallback — with the fallback gone the prefix had nothing left to say and it was removed. Nothing you do is affected: the bytes underneath are unchanged.

Amazon SES notifications

SES replaces the envelope sender with its own, so no return path can collect its bounces. It reports over SNS instead. See Bounce Handling .

There is nothing to configure here. POST /webhooks/ses always exists, and the SNS topic ARN is set on the SMTP server that uses SES — under SMTP Servers, or Shared SMTP Pool for a platform-owned one.

That is deliberate. SES belongs to one server: a project configures its own SES account as its own server, so a platform-wide list could only ever serve an operator who owned the account themselves, and a tenant had no way to register a topic at all.

The endpoint is public, because SNS presents no session and no API key. Two things authenticate a notification, and it needs both: the SNS signature, verified against a certificate fetched only from sns.<region>.amazonaws.com, and the topic being configured on a server. The signature alone proves Amazon sent the message, not that your account did — anyone can open an AWS account and point a topic at your URL.

With no server carrying a topic, nothing is accepted. That is why the endpoint needs no switch: it gates itself on the data.

A notification is additionally required to be about a message that actually left through a server publishing to that topic. Attribution still comes from the X-Mailyard-Email-Id header — this is what stops one tenant’s topic reporting on another tenant’s mail.

Metrics

VariableDefaultDescription
MAILYARD_METRICS_ENABLEDfalseBinds the Prometheus scrape listener and serves GET /metrics on it
MAILYARD_METRICS_ADDR127.0.0.1:9090Where that listener binds. Its own port
MAILYARD_METRICS_TOKENWhen set, the endpoint requires it as a bearer token. Needed once the bind is widened

Rate Limiting

Fixed-window counters held in process memory, so on a multi-node deployment the effective ceiling is the value here times the node count. Size accordingly, or terminate the limit at a shared proxy instead. Zero on an individual limit disables just that one.

VariableDefaultDescription
MAILYARD_RATELIMIT_ENABLEDtrueMaster switch for all three HTTP limiters
MAILYARD_RATELIMIT_LOGIN_PER_MINUTE10POST /app/api/auth/login per client IP
MAILYARD_RATELIMIT_OIDC_PER_MINUTE30OIDC callback per client IP
MAILYARD_RATELIMIT_API_PER_MINUTE120/api/v1 per API key, or per IP for callers with no usable token

Per-project sending volume is a separate mechanism: it comes from the plan assigned to the project, not from configuration. See Plans . The SMTP listeners have their own per-IP session limits, MAILYARD_SUBMISSION_RATE_PER_MINUTE and MAILYARD_INBOUND_RATE_PER_MINUTE.

Worker

The delivery worker runs inside the server process, so there is nothing to enable. Running more nodes runs more workers - every serve node claims from the same queue.

Exactly one node carries --init, which applies pending migrations. The rest start without it and skip goose entirely. Two nodes migrating an empty database at the same time race, and a node that starts before anybody has applied the schema says so rather than failing on the first query. See Scaling out .

These settings are per node. Three nodes at concurrency 4 give you twelve parallel deliveries, not four.

VariableDefaultDescription
MAILYARD_WORKER_CONCURRENCY4Parallel delivery goroutines
MAILYARD_WORKER_POLL_INTERVAL2sHow often the queue is checked for due work. A send also wakes the worker immediately - on every node, not just the one that accepted it - so this only governs scheduled and retried mail
MAILYARD_WORKER_MAX_ATTEMPTS5Delivery attempts before an email is marked failed
MAILYARD_WORKER_RETRY_BASE_DELAY30sSeeds the exponential backoff: base × 2^(attempt−1)
MAILYARD_WORKER_RETRY_MAX_DELAY1hCeiling for that backoff
MAILYARD_WORKER_CLAIM_TIMEOUT5mRe-queues processing rows older than this, which is how a crashed node’s in-flight mail is recovered. Keep it comfortably above your slowest SMTP delivery, or a slow send is re-queued while it is still running

Campaigns

VariableDefaultDescription
MAILYARD_CAMPAIGN_BATCH_SIZE100Recipients rendered and queued per batch. The campaign’s own send rate throttles between batches
MAILYARD_CAMPAIGN_POLL_INTERVAL5sHow often the runner looks for due campaigns

Webhooks

VariableDefaultDescription
MAILYARD_WEBHOOK_MAX_ATTEMPTS3Total delivery attempts per event, including the first
MAILYARD_WEBHOOK_TIMEOUT10sPer-request timeout, as a Go duration
MAILYARD_WEBHOOK_RETRY_DELAY10sFixed wait between attempts, as a Go duration
MAILYARD_WEBHOOK_ALLOW_PRIVATE_TARGETSfalsePermit deliveries to private and reserved addresses

Private webhook targets

Webhook URLs are chosen by project members, not operators. By default a delivery to a loopback, RFC 1918, link-local or otherwise reserved address is refused at connection time, which stops a member from aiming one at the cloud metadata service (169.254.169.254), at a neighbouring container, or at this process’s own admin API and using the platform as a proxy into your network. Redirects are not followed, for the same reason.

Set MAILYARD_WEBHOOK_ALLOW_PRIVATE_TARGETS=true only when your receivers really do live on the same private network as this deployment, and understand that it re-opens that path to anyone who can create a webhook.

Delivery

VariableDefaultDescription
MAILYARD_SENDING_AUTO_SUPPRESS_ON_REJECTtrueAdd a recipient to the suppression list (and stop retrying) after a permanent 5xx rejection at RCPT TO, e.g. 550 user unknown

Email Verification

Controls the POST /api/v1/emails/verify endpoint: syntax, disposable and role-account checks, and an MX lookup with an A/AAAA fallback per RFC 5321. Off by default because the MX check makes outbound DNS queries, which an operator on a locked-down network should opt into rather than discover. Results are cached in process memory, so each node builds its own cache and the TTLs below are per node.

VariableDefaultDescription
MAILYARD_EMAIL_VERIFY_ENABLEDfalseEnable the endpoint
MAILYARD_EMAIL_VERIFY_CACHE_TTL24hHow long an address-level verdict is reused, as a Go duration
MAILYARD_EMAIL_VERIFY_MX_CACHE_TTL1hHow long a domain’s MX answer is reused
MAILYARD_EMAIL_VERIFY_LOOKUP_TIMEOUT5sBounds one DNS resolution

There is deliberately no SMTP probe, so mailbox_verified is always false. See Email Verification for why.

Platform mail

The platform’s own outbound mail: project invitations, password resets and signup confirmations. Deliberately separate from the tenant send pipeline, so it never consumes a project’s plan quota, appears in a project email log, or needs a tenant to have configured an SMTP server.

There is nothing to configure here. There used to be a system_mail block with its own host, port and credentials — it is gone. It meant an operator configured platform SMTP twice, once in the file and once in the console, and the console could show neither.

Platform mail now leaves through the shared SMTP pool, and the address it sends from is a platform setting rather than config:

SettingWhereDescription
platform_mail_fromAdmin → SettingsEnvelope sender. Empty means platform mail is off
platform_mail_from_nameAdmin → SettingsOptional display name
platform_onlyAdmin → Shared SMTP Pool, per serverReserves that server for platform mail, so no tenant is routed through it

The address is a setting and not a config key on purpose: everything else about the pool is edited in the console by the same administrator, and an address that needs a restart to correct is the one thing they cannot fix when it is wrong.

platform_mail_from requires MAILYARD_SERVER_PUBLIC_URL, since every link it sends is absolute — and that requirement is checked at the moment somebody writes the address, which is where it is actionable.

With no address set, invitations still work — the console returns a copyable link instead — but password reset is not offered at all. Status and a connection test live at GET/POST /api/v1/admin/system-mail[/test]. See System Mail .

Relay nodes

A relay node is a machine somewhere else — often another provider, often another continent — that delivers straight to recipient mail exchangers from its own address, and can run an MX of its own. Two config blocks, and they point in opposite directions.

The control plane, on an API node

VariableDefaultDescription
MAILYARD_RELAY_NODES_ENABLEDfalseRegisters the enrolment endpoints at all. Off means the routes do not exist rather than refusing
MAILYARD_RELAY_NODES_AUTO_REGISTER_TOKENThe shared secret a platform node presents to enrol. Required when enabled
MAILYARD_RELAY_NODES_CA_COMMON_NAMEMailyard Relay CANames the authority in its own certificate

A leaked token is not a disaster on its own: enrolment lands in pending, and a node carries no mail until somebody approves it. Whether that approval is automatic is the relay_nodes_auto_approve platform setting, off by default.

A project’s own node enrols with an API key carrying relay:enroll instead of this token, and is approved by an administrator of that project.

The node itself

Read only by mailyard relay. A node ships neither the database DSN nor the encryption key.

VariableDefaultDescription
MAILYARD_RELAY_NODE_CONTROL_URLWhere the platform lives. The node only ever connects out
MAILYARD_RELAY_NODE_ENROLL_TOKENThe shared secret, or a project API key. First run only
MAILYARD_RELAY_NODE_ADDR:2587Where delivery workers connect. Implicit TLS, client certificate required
MAILYARD_RELAY_NODE_HOSTNAMEThe certificate name workers dial AND the HELO it announces
MAILYARD_RELAY_NODE_SERVER_GROUPSlug of the project server group to join. Ignored by a platform node
MAILYARD_RELAY_NODE_SPOOL_DIR./relay-spoolThe queue. Metadata in bbolt, message bytes as files beside it
MAILYARD_RELAY_NODE_MAX_LIFETIME72hHow long a message may keep failing before it is given up on
MAILYARD_RELAY_NODE_HEARTBEAT_INTERVAL2mHow often the node reports in
MAILYARD_RELAY_NODE_DELIVERY_CONCURRENCY8Simultaneous outbound sessions
MAILYARD_RELAY_NODE_SMTP_PORT25Destination port. Configurable only so a test can point it elsewhere
MAILYARD_RELAY_NODE_IPV6falseAllow delivery over IPv6

IPv6 is off by default, and that default is the point: a box with a AAAA address will happily prefer v6, where receivers want a PTR and SPF coverage a v4-only setup does not have, and the mail is refused for reasons that look nothing like the cause.

The control channel is plain HTTPS with a nil transport, so HTTPS_PROXY, ALL_PROXY and SOCKS5 work with no configuration — which is the whole point for a node behind a national firewall or a VPN.

The node verifies your certificate

It uses the system trust store. If the platform serves a self-signed certificate, no node will enrol until that root is installed on each machine.

A node that also receives

VariableDefaultDescription
MAILYARD_RELAY_NODE_INBOUND_ENABLEDfalseTake mail on port 25 and forward it over the control channel
MAILYARD_RELAY_NODE_INBOUND_ADDR:25Where the internet delivers
MAILYARD_RELAY_NODE_INBOUND_MAX_MESSAGE_SIZE26214400Bytes per received message
MAILYARD_RELAY_NODE_INBOUND_RATE_PER_MINUTE120New sessions per client IP per minute
MAILYARD_RELAY_NODE_INBOUND_TLS_CERTSTARTTLS certificate on the node’s own disk. Empty generates a self-signed pair
MAILYARD_RELAY_NODE_INBOUND_TLS_KEYThe matching key. Both or neither

Point the MX record at the node rather than at Mailyard. See Relay Nodes .

Sending Limits

Caps applied to every send, on both the console API and the machine API.

VariableDefaultDescription
MAILYARD_SENDING_MAX_RECIPIENTS50Recipients per message
MAILYARD_SENDING_MAX_ATTACHMENT_SIZE10485760Bytes per attachment (10 MiB)
MAILYARD_SENDING_MAX_TOTAL_ATTACHMENT_SIZE26214400Bytes of attachments per message (25 MiB)
MAILYARD_SENDING_ALLOW_PRIVATE_SMTP_TARGETSfalseLet a project’s SMTP server point at a private or reserved address

These size limits set the HTTP body limit

Attachments travel base64-encoded, which inflates them by 4/3, so the request body cap is computed from max_total_attachment_size at startup rather than configured separately. That cap applies only to the routes that carry attachments (/api/v1/emails/send, /send-template, /batch, template attachment uploads and template import). Every other API route takes 8 MiB, and everything outside the API - login, webhooks, tracking - takes 1 MiB. Lower these values if the deployment does not send large files.

Two configurations are refused at startup: a max_attachment_size larger than max_total_attachment_size (unreachable, and it reads as a promise the total overrides), and a total above 256 MiB.

See Attachments for the request shape and the GET /api/v1/emails/limits endpoint that reports the effective values.

Sender Authentication

VariableDefaultDescription
MAILYARD_SENDING_BOUNCE_ADDRESSReturn path for mail leaving through the shared platform pool only, where the sending IPs are ours. A project sending through its own server sets its own under Project settings. Empty leaves MAIL FROM as the From address. See Bounce Handling
MAILYARD_SENDING_SPF_INCLUDEHost tenants should name in their SPF record, e.g. _spf.mail.example.com. Empty means the console tells the operator to set it rather than printing a placeholder.
MAILYARD_INBOUND_REJECT_ON_DMARC_FAILfalseRefuse received mail when the From domain publishes p=reject and nothing it vouches for passed

Outbound mail is DKIM signed automatically for every domain a project has verified. Keys are RSA-2048, generated when ownership verifies, and the private half is encrypted with database.crypto.encryption_key before it reaches the database. There is no variable to turn signing on: a verified domain with a key gets signed mail.

Inbound mail is always authenticated and the verdict always stored. MAILYARD_INBOUND_REJECT_ON_DMARC_FAIL only decides whether a failure is also a refusal. See Domain Verification .

Attachment Storage

Where attachment bytes live. The default keeps them inline as base64 in the database, which needs no configuration at all. The fs and s3 backends move the bytes out and store only metadata plus a storage key.

VariableDefaultDescription
MAILYARD_STORAGE_BACKENDEmpty for inline, or fs or s3
MAILYARD_STORAGE_FS_PATHdata/attachmentsBase directory for the fs backend
MAILYARD_STORAGE_S3_ENDPOINTS3-compatible endpoint (e.g. MinIO, R2)
MAILYARD_STORAGE_S3_REGIONS3 region
MAILYARD_STORAGE_S3_BUCKETS3 bucket name, required for the s3 backend
MAILYARD_STORAGE_S3_ACCESS_KEYS3 access key
MAILYARD_STORAGE_S3_SECRET_KEYS3 secret key
MAILYARD_STORAGE_S3_USE_PATH_STYLEfalsePath-style addressing, required by some MinIO setups

Inbound Email

VariableDefaultDescription
MAILYARD_INBOUND_ENABLEDfalseMaster toggle — enables the MX listener
MAILYARD_INBOUND_ADDR:25Bind address, host and port together. 25 is where MX delivery arrives, and it is privileged: see Privileged ports
MAILYARD_INBOUND_HOSTNAMEmailyardHostname announced in the SMTP EHLO greeting — should match the MX record
MAILYARD_INBOUND_MAX_MESSAGE_SIZE26214400Max raw message size in bytes (default 25 MiB)
MAILYARD_INBOUND_RATE_PER_MINUTE120Per-IP max SMTP sessions per minute (0 disables)
MAILYARD_INBOUND_REJECT_ON_DMARC_FAILfalseSee Sender Authentication
MAILYARD_INBOUND_TLS_ENABLEDtrueOffer STARTTLS

Recipients are gated at RCPT TO on domains the project verified by DNS TXT — there is no authentication on this listener, and mail for anything unclaimed is refused. See Inbound Email .

SMTP submission

Outbound submission for applications that speak SMTP rather than HTTP. Off by default, MAILYARD_SUBMISSION_ENABLED=true turns it on and MAILYARD_SUBMISSION_ADDR defaults to :587. The full table, including AUTH and STARTTLS, is in SMTP Submission .

CORS

Off by default, and it should stay off unless something needs it: the console is served from the same origin as the API, so it never does. This exists for a separately hosted front end or a browser-based integration.

cors:
    enabled: true
    allowed_origins:
        - https://app.example.com
    allow_credentials: true
KeyEnvDefaultDescription
cors.enabledMAILYARD_CORS_ENABLEDfalseTurn CORS on
cors.allowed_originsMAILYARD_CORS_ALLOWED_ORIGINS-Full origins including the scheme. Required when enabled.
cors.allowed_methodsMAILYARD_CORS_ALLOWED_METHODSGET,POST,PUT,PATCH,DELETE,OPTIONS
cors.allowed_headersMAILYARD_CORS_ALLOWED_HEADERSContent-Type,Authorization,X-Mailyard-Project-Id
cors.expose_headersMAILYARD_CORS_EXPOSE_HEADERS-Response headers the browser may read
cors.allow_credentialsMAILYARD_CORS_ALLOW_CREDENTIALSfalseLet the browser send the session cookie cross-origin
cors.max_ageMAILYARD_CORS_MAX_AGE300Preflight cache, in seconds

The configuration is checked at startup and the server refuses to boot on these, rather than leaving you to debug a silent browser rejection:

  • enabled with no origins listed.
  • An origin without a scheme (app.example.com instead of https://app.example.com).
  • allowed_origins: ["*"] together with allow_credentials: true - browsers reject that combination anyway, and configuring it means somebody expected cookies to work.

allow_credentials is the dangerous switch

It turns every listed origin into somewhere an authenticated request can be made from. List only origins you control.

TLS

Two separate questions, answered in two separate places, and keeping them apart is the point.

Whether a listener terminates TLS is configuration. It is a port-level decision — the common deployment puts a reverse proxy in front that terminates TLS and speaks HTTP upstream — and it needs a restart either way.

VariableDefaultDescription
MAILYARD_SERVER_TLS_ENABLEDfalseHTTPS on the console and API
MAILYARD_SUBMISSION_TLS_ENABLEDtrueSTARTTLS on the submission listener
MAILYARD_INBOUND_TLS_ENABLEDtrueSTARTTLS on the MX listener

The HTTP default differs from the two SMTP ones deliberately. On 25 and 587 a client prefers encryption and almost none verify the certificate, so even a self-signed pair is a real improvement over cleartext. In front of the console there is usually a proxy, and terminating here would make it talk HTTPS upstream to a certificate it has no reason to trust. Relay nodes are the sharper half of that: a node verifies this certificate with its system trust store, so turning SERVER_TLS_ENABLED on with a self-signed pair stops every node enrolling until you install the root on each machine.

Which certificate is served is not configured in this file. It is assigned in the console under Administration → Certificates, stored in the database, and resolved per handshake — so a replacement takes effect with no restart and reaches every node. Each listener walks one chain:

  1. the certificate assigned to it, if any
  2. the ACME certificate, if ACME is configured for that hostname
  3. a self-signed pair, generated on first use and shared by every node

A listener always has something to present, and an unreachable console is recoverable with mailyard tls unassign --listener server, which writes to the database with no server running.

ACME

One block for the whole installation, covering every listener above.

VariableDefaultDescription
MAILYARD_ACME_ENABLEDfalseOrder certificates from Let’s Encrypt
MAILYARD_ACME_HOSTSderivedHostnames to issue for. Defaults to the host in server.public_url
MAILYARD_ACME_EMAILRegistration contact, where the CA sends expiry warnings
MAILYARD_ACME_CHALLENGE_ADDR:80HTTP-01 challenge listener, must be reachable as port 80 from the internet

A name that is not in hosts falls through to the self-signed pair rather than failing the handshake. That is the ordinary state of an MX: public_url names the console, so the derived list names the console, and the mail listeners answer under a different hostname.

There is no cache directory. Issued certificates go into the database, so every node serves the same one and a restart re-issues nothing. Renewal happens about 30 days before expiry, and Mailyard touches each configured host at startup and hourly after that, so a listener that has seen no traffic still renews.

Example environment

# Server
MAILYARD_SERVER_ADDR=:3000
MAILYARD_SERVER_PUBLIC_URL=https://mail.example.com
MAILYARD_SERVER_TRUSTED_PROXIES=10.0.0.0/8

# Database
MAILYARD_DATABASE_DSN=postgres://mailyard:secure-password@localhost:5432/mailyard?sslmode=require

# Secrets - generate both with `openssl rand -hex 32`
MAILYARD_AUTH_JWT_SECRET=...
MAILYARD_DATABASE_CRYPTO_ENCRYPTION_KEY=...

# Sign-in
MAILYARD_AUTH_LOCAL_ENABLED=true
MAILYARD_AUTH_LOCAL_EMAIL=admin@example.com

# HTTPS here rather than at a proxy. Which certificate is served is
# chosen in the console, not set here.
MAILYARD_SERVER_TLS_ENABLED=true
MAILYARD_ACME_ENABLED=true
MAILYARD_ACME_EMAIL=ops@example.com

# Optional SMTP listeners, both off unless enabled
MAILYARD_SUBMISSION_ENABLED=true
MAILYARD_INBOUND_ENABLED=true

# Observability
MAILYARD_LOGGING_FORMAT=json
MAILYARD_METRICS_ENABLED=true
MAILYARD_METRICS_TOKEN=...

On first start against an empty database the bootstrap admin password is written to stderr once. Migrations run automatically, so nothing else is needed.

Platform mail — invitations, password resets, signup confirmations — is not in this list because it is not configured here. Set platform_mail_from in Admin → Settings and give the shared pool a server. See Platform mail .