Postmark Email Setup

How to switch an environment from the dev-only logging sender to real transactional email via Postmark. See transactional-email-sending for the design and Instance License Onboarding for the license half of onboarding.

⚠️ Fail-fast contract — read first

The server refuses to boot (k8s crashloop) when EMAIL_PROVIDER=postmark is set and EMAIL_TOKEN or EMAIL_FROM is missing, empty, or whitespace — and when EMAIL_PROVIDER itself has an unrecognized value (accepted: logging, postmarksmtp is rejected until an SMTP sender exists; the value is trimmed, but case matters — Postmark is rejected). This is by design: a silent fallback to the logging sender would write live reset tokens into Loki and send no real email, which is invisible breakage of migrated-user onboarding. The boot error names the variable to fix.

Operational consequence: flip the provider and its companion secrets in the same change. Never set EMAIL_PROVIDER=postmark for an environment without EMAIL_TOKEN + EMAIL_FROM landing alongside it.

Prerequisites (one-time, Postmark account)

  1. A verified sender signature or domain for the From address (no-reply@… — domain choice is tracked as an open question in transactional-email-sending).
  2. A server token per environment (staging and production must not share), and a Message Stream per environment so staging volume never touches production sending reputation.

Per-environment secrets (GitHub → Settings → Environments)

Set in the staging and production GitHub environments (same names, distinct values — the deploy jobs are environment-scoped):

SecretValueNotes
EMAIL_PROVIDERpostmarkexact lowercase; unset = logging sender
EMAIL_TOKENthe env’s Postmark server tokensecret — never committed, never in libs/core
EMAIL_FROMe.g. no-reply@<verified-domain>must match a verified Postmark signature/domain
EMAIL_MESSAGE_STREAMe.g. staging-transactional / outboundoptional; Postmark defaults to outbound

Delivery path: workflow → apply-secrets action → k8s server-env secret → Deployment envFrom → server env → Config::apply_env_overrides. The action only writes non-empty values and never drops existing keys, so an empty GitHub secret leaves the k8s key untouched (it does NOT unset it — to go back to the logging sender, set EMAIL_PROVIDER=logging, don’t just blank it).

Verify after the deploy

  1. Boot log shows Email sender: Postmark HTTP API (with the from-address and stream) instead of a logging-sender line.
  2. Trigger a password reset against the environment; the email arrives and its link points at the environment’s app host.
  3. email_send_failures_total (Prometheus) stays flat; any increment is a redacted warn in the logs with error_kind/error_code — code 406 means the recipient address is suppressed at Postmark (stale migrated address), not that sending is broken.

Rollback

Set EMAIL_PROVIDER=logging for the environment and redeploy. Positive confirmation (mirror of forward check #1): the boot log shows Email sender: logging (dev only — bodies, including tokens, go to logs). Reset/verify links then appear in server logs only (tokens in logs — acceptable in staging, avoid leaving production this way for long).