Skip to content

fix(web): ship email templates and load postmark in the production web image - #294

Merged
jpr5 merged 3 commits into
mainfrom
fix/issue-253-email-templates
Oct 28, 2026
Merged

jpr5 merged 3 commits into
mainfrom
fix/issue-253-email-templates

Conversation

@jpr5

@jpr5 jpr5 commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #253

Problem

Email could not work in the production outpost-web image, for two separate reasons:

  1. Templates missing. templates/ is at the repo root, outside every workspace package. turbo prune --docker leaves it out of out/full/, and the runner stage never copied it. findTemplatesDir() found nothing, so every sendEmail returned Template "…" not found and invites came back with emailSent: false. (.dockerignore's *.md is root-level only, so the files are in the build context; the pruner stage has them.)
  2. Postmark nulled in the server bundle. next.config.ts aliased postmark to false for the server build. sendEmail is transpiled into that bundle (transpilePackages), so new ServerClient() threw, createPostmarkTransport() returned null, and the send fell through to the console fallback. This was masked by (1).

Fix

  • apps/web/Dockerfile: runner stage copies /app/templates from the pruner stage. The standalone server chdirs to /app/apps/web, and the loader's ../../templates candidate resolves to /app/templates.
  • apps/web/next.config.ts: drop postmark from the server null-alias list. nodemailer stays nulled (not installed; prod has no SMTP_* config, Postmark is the transport). The rest of the file diff is Prettier reflow, which CI's changed-file format check requires.
  • apps/web/src/__tests__/production-email-image.test.ts: guards that the runner stage copies templates to /app/templates and that the server webpack config does not alias postmark. Both assertions fail on main and pass here.

The worker image is unchanged: the worker does not load templates or send email.

Red-green proof (real surface: the built image)

Harness: built apps/web/Dockerfile locally, ran it next to Postgres and a fake api.postmarkapp.com (TLS sink on a Docker network alias, NODE_TLS_REJECT_UNAUTHORIZED=0, fake POSTMARK_SERVER_TOKEN). Real flow: /api/setup → credentials sign-in → POST /api/templates/invite/preview → POST /api/team/invite. No real email left the machine. Tokens redacted.

RED: main

== outpost-web:red: filesystem
ls: /app/templates: No such file or directory
== template render (POST /api/templates/invite/preview): 404 Template not found
== invite (POST /api/team/invite): 200 emailSent=false
[INVITE] Failed to send invite email to invitee@example.test: Template "invite" not found
== Postmark sink log
SINK listening on 443            <- nothing received

RED: templates fixed, postmark alias still in place (isolates the second bug)

== template render (POST /api/templates/invite/preview): 200 subject="You've been invited to join Acme Corp"
[Email] POSTMARK_SERVER_TOKEN is set but postmark package failed to load: TypeError: b is not a constructor
    at async z (.next/server/app/api/team/invite/route.js:1:3857)
=== EMAIL (dev mode) ===        <- fell back to console, printing the invite link
== Postmark sink log
SINK listening on 443            <- nothing received

GREEN: this branch (image rebuilt from the final commit)

== outpost-web:green: filesystem
/app/templates: digest.md escalation.md invite.md sla-breach.md ticket-created.md ticket-resolved.md welcome.md
== template render (POST /api/templates/invite/preview): 200 subject="You've been invited to join Acme Corp" html_bytes=1328
== invite (POST /api/team/invite): 200 (emailSent not set to false)
== Postmark sink log
SINK POST /email HTTP/1.1 token=<redacted>
SINK BODY {"From":"Proof Org <org@example.test>","To":"invitee@example.test","Subject":"You've been invited to join Proof Org","HtmlBody":"<!DOCTYPE html>…","TextBody":"Hi invitee,\n\nYou've been invited to join Proof Org on Outpost.…","MessageStream":"outbound"}

Guard test: main → 2 failed / 1 passed. This branch → 3 passed. Full apps/web suite: 859 passed. Typecheck clean.

Still needed for email to actually send in production

Production outpost-web has no POSTMARK_SERVER_TOKEN (and no SMTP_*). Until a token is set, sendEmail uses the console fallback. Sender domain DNS is #215.

jpr5 added 3 commits October 28, 2026 14:30
templates/ sits at the repo root, outside every workspace package, so turbo
prune drops it from out/full/ and the runner stage never copied it. Every
sendEmail call in the image returned 'Template "…" not found'.

Refs #253
…bundle

sendEmail is transpiled into the web server bundle, so nulling postmark made
new ServerClient() throw and every Postmark send fall back to console output.

Refs #253
@jpr5
jpr5 merged commit 3579af3 into main Oct 28, 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.

templates/ never reaches the production image, so every email send fails with emailSent: false

1 participant