Skip to content

fix(config): production refuses an unset or example FROM_EMAIL - #103

Merged
jayleaton merged 3 commits into
mainfrom
fix/production-from-email
Oct 23, 2026
Merged

jayleaton merged 3 commits into
mainfrom
fix/production-from-email

Conversation

@jayleaton

@jayleaton jayleaton commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Both apps fall back to FROM_EMAIL=noreply@example.com when it is unset, and production accepted that sender. No mail provider sends from it, so every verification, reset and admin invite email failed, visible only as a log line.

Summary

 config.Load (apps/web and apps/admin), APP_ENV=production:
+  FROM_EMAIL unset                        → "FROM_EMAIL is required in production (a sender on your verified email domain)"
+  sender on a reserved example/test domain → "FROM_EMAIL must be a sender on your verified email domain in production, got %q"

"Reserved" means example.com|net|org, .example, .test, .invalid, .localhost and their subdomains (RFC 2606/6761), in either a@b or Name <a@b> form. Development and test keep the noreply@example.com default. README ("Every service runs with…") and .env.example say so.

Evidence

Before (main, APP_ENV=production, RESEND_API_KEY set, FROM_EMAIL unset): both boot and report healthy.

web   {"msg":"listening","addr":"0.0.0.0:39300","env":"production"}   /healthz → {"data":{"status":"ok"},"error":null}
admin {"msg":"admin listening","addr":"0.0.0.0:39301","env":"production"}   /healthz → {"data":{"status":"ok"},"error":null}

After (same env; each line is web, then admin):

FROM_EMAIL unset                     → invalid config: FROM_EMAIL is required in production (a sender on your verified email domain)
                                       invalid admin config: FROM_EMAIL is required in production (…)
FROM_EMAIL=noreply@example.com       → invalid config: FROM_EMAIL must be a sender on your verified email domain in production, got "noreply@example.com"
FROM_EMAIL=hello@mail.example.org    → refused (both)
FROM_EMAIL=ops@app.test              → refused (both)
FROM_EMAIL=Acme <noreply@example.com>→ refused (both)
FROM_EMAIL=Acme <noreply@acme.io>    → listening (both)
FROM_EMAIL=noreply@acme.io           → listening (both)
APP_ENV=development, FROM_EMAIL unset→ listening (both)

Config is not a test boundary (AGENTS.md), so these are reproduction runs of the built binaries, not unit tests.

Checks: make check-fast ✅. make generate lint test ✅ (all coverage floors pass).

Depends on: deploy plan/setup rejecting the example sender

deploy/platform.yaml (and platform.DefaultConfig) default email.from: noreply@example.com, which railway-setup writes into FROM_EMAIL. With this PR alone, a fresh make railway-setup on the defaults deploys services that refuse to boot until email.from is set. That refusal is the point of the fix, but the deploy tooling should catch it first. The deploy-bugs work (branch fix/template-deploy-bugs, which owns deploy/ and railway-setup) is making plan/setup reject the noreply@example.com default. Merge it with or before this PR. This PR does not touch deploy/.

Merge Danger

Door: two-way. It is config validation only, and a revert restores the old boot behaviour.

Blast Radius: deployments. Any production web or admin deployment with an unset or example FROM_EMAIL stops booting on its next deploy and shows the reason. Its emails were already failing.

🤖 Generated with Claude Code

jayleaton added a commit that referenced this pull request Oct 23, 2026
Every Railway service runs with APP_ENV=production, which refuses an
example.com (.test, ...) FROM_EMAIL at boot (#103), and platform.yaml
defaults email.from to noreply@example.com. A fresh apply would deploy a
crash loop. Plan and apply now fail before any Railway call, and doctor
reports a failing email.from check with the fix; a TODO from_email is
left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jayleaton
jayleaton force-pushed the fix/production-from-email branch from cd9e282 to eba3863 Compare October 23, 2026 12:12
@jayleaton

Copy link
Copy Markdown
Owner Author

Rebased onto main 5d72efe. The config change is as written: both apps' config.Load refuse an unset or reserved-domain FROM_EMAIL in production, and it already passes the strict name and magic-number rules. The README sentence moved to docs/deploy.md after #107.

Added one commit: scripts/release-smoke.sh (from #115, newer than this PR) boots web and admin with only what production requires, and that set now includes FROM_EMAIL. Its first CI run failed with exactly this PR's error (web migrate failed: FROM_EMAIL is required in production), so the smoke now sets release-smoke@go-fast-stack.dev. Nothing is sent; the Resend key is a dummy.

Checked on the rebased web binary with APP_ENV=production: unset → FROM_EMAIL is required in production …; noreply@example.com and Acme <noreply@example.com> → must be a sender on your verified email domain …; noreply@acme.io → no FROM_EMAIL error. make check-fast ok, ci-ok success on fc2d8b1 (smoke: web and admin /healthz 200).

Not merged yet, per this PR's own "Depends on": main's deploy/platform.yaml and platform.DefaultConfig still default email.from: noreply@example.com, and no merged or open PR rejects it in plan/setup. Merged alone, a fresh make railway-setup on the defaults would deploy web and admin into this boot refusal. Waiting on that deploy-side check, or a call to merge without it.

jayleaton added a commit that referenced this pull request Oct 23, 2026
Every service runs with APP_ENV=production, and the apps refuse to boot
with an unset FROM_EMAIL or one on a reserved example/test domain (#103).
deploy/ defaulted email.from to noreply@example.com, so a fresh make
railway-setup on defaults would deploy apps that never boot.

- platform: RealSender, the apps' exact rule (example.com/.net/.org,
  example, test, invalid, localhost; "Name <a@b>" allowed), and
  SenderProblems per environment (email.from or
  per_environment.<env>.from_email; manual is not checked). email.from
  now defaults to unset.
- railway-setup: plan, setup and apply refuse before touching anything,
  with a next step; apply on a terminal without --yes asks for the
  sender and writes it into platform.yaml. New --email-from /
  PLATFORM_EMAIL_FROM (platform.Edit now sets dotted keys). doctor fails
  the email.from check.
- docs/deploy.md and AGENTS.md: a real sender is required.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jayleaton and others added 3 commits October 23, 2026 19:23
Both apps fell back to noreply@example.com when FROM_EMAIL was unset, and
accepted it in production, where no mail provider sends from it: every
verification, reset and invite email would fail, visible only in the log.
In production the web and admin configs now refuse an unset FROM_EMAIL and
any sender on a reserved example or test domain (RFC 2606/6761), in either
"a@b" or "Name <a@b>" form, so the deploy fails its boot with the reason.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…domains

Production now refuses an unset or example sender, so the smoke (which
boots with only what production requires) sets one. Nothing is sent: the
Resend key is a dummy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The apps' realSender and deploy/platform's reservedSenderDomains (#132)
refuse the same domains; both now say so. docs/deploy.md links to #132's
paragraph instead of repeating it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jayleaton
jayleaton force-pushed the fix/production-from-email branch from fc2d8b1 to 743c773 Compare October 23, 2026 12:23
@jayleaton

Copy link
Copy Markdown
Owner Author

The dependency is in: #132 (ece908d) makes plan, setup and apply refuse an unset or example email.from. Rebased onto it, keeping the release-smoke commit. #132's reservedSenderDomains is the same list as this PR's realSender (example.com, example.net, example.org, example, test, invalid, localhost and their subdomains, same suffix matching), so nothing to align. Added one small commit: both apps' realSender now point back at reservedSenderDomains ("keep the two lists the same", as #132 does), and docs/deploy.md links to #132's "A real sender is required" paragraph instead of repeating it. make check-fast ok, make lint-layering ok (112 probes), ci-ok success on 743c773.

@jayleaton
jayleaton merged commit 3b13c57 into main Oct 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant