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, postmark — smtp 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)
- A verified sender signature or domain for the
Fromaddress (no-reply@…— domain choice is tracked as an open question in transactional-email-sending). - 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):
| Secret | Value | Notes |
|---|---|---|
EMAIL_PROVIDER | postmark | exact lowercase; unset = logging sender |
EMAIL_TOKEN | the env’s Postmark server token | secret — never committed, never in libs/core |
EMAIL_FROM | e.g. no-reply@<verified-domain> | must match a verified Postmark signature/domain |
EMAIL_MESSAGE_STREAM | e.g. staging-transactional / outbound | optional; 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
- Boot log shows
Email sender: Postmark HTTP API(with the from-address and stream) instead of a logging-sender line. - Trigger a password reset against the environment; the email arrives and its link points at the environment’s app host.
email_send_failures_total(Prometheus) stays flat; any increment is a redacted warn in the logs witherror_kind/error_code— code406means 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).