Postmark Email Provider
Transactional email delivery for emdash via the Postmark API.
What it does
This plugin registers an email:deliver exclusive hook in emdash's email pipeline. When any plugin calls ctx.email.send() (like the forms plugin sending notification emails), the message is delivered through Postmark's API.
Without an email provider plugin, emdash has no production email delivery. The built-in console provider only works in dev mode. This plugin fills that gap for Cloudflare Workers deployments.
Setup
Copy the plugin source into your emdash site directory (it needs to import definePlugin from emdash):
sites/your-site/plugins/postmark-email.tsRegister it in astro.config.mjs:
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
// In the emdash() plugins array:
{
id: "postmark-email",
version: "0.1.0",
entrypoint: resolve(__dirname, "plugins/postmark-email.ts"),
// Build-time options only. Vite's loadEnv resolves these on the build
// machine and writes the literal values into the deployed bundle, so the
// server token must NOT go here. It is read from plugin KV at delivery.
options: {
fromAddress: env.POSTMARK_FROM_ADDRESS,
},
capabilities: ["hooks.email-transport:register", "network:request"],
allowedHosts: ["api.postmarkapp.com"],
}Configuration
Only the from address is a build-time option. It is not secret, and it must be a verified sender signature in Postmark. Put it in a gitignored .env:
POSTMARK_FROM_ADDRESS=noreply@yourdomain.comWhere the server token goes, and where it must not
Not in .env, and not in the plugin's options. Vite's loadEnv resolves those on the build machine and writes the literal value into dist/server/chunks/env_*.mjs, which wrangler deploy uploads. We shipped a token that way and had to rotate it. emdash fixed the same class of bug for its own secrets in 0.31.0 (#2140), and that changeset's advice is the right rule: anything that has been inside a bundle is burned.
Read the token at runtime from plugin KV instead. A Cloudflare Worker secret would be a stronger store, but PluginContext exposes no env accessor as of emdash 0.32.0, so a plugin cannot reach one.
# The server token lives in plugin KV, which persists in the
# `options` table under plugin:<pluginId>:<key>. Set or rotate it with:
npx wrangler d1 execute <your-db> --remote --command \
"INSERT INTO options (name, value) VALUES
('plugin:postmark-email:settings:serverToken', json_quote('<token>'))
ON CONFLICT(name) DO UPDATE SET value = excluded.value;"
# In the hook handler, read it at delivery time:
const token = await ctx.kv.get("settings:serverToken");One caveat worth knowing: EMDASH_ENCRYPTION_KEY is validated at startup but currently inert, so plugin secrets sit unencrypted in the database.
Formerly required: a post-build patch
This plugin used to need a post-build patch because plugin route handlers never received ctx.email (emdash-cms/emdash#215, which we reported). It was fixed upstream in emdash 0.2.0, six days after we filed it. Do not re-apply it.
A warning from carrying it too long: our patch script exited 0 with "Already patched or pattern not found" when its target was gone, so once upstream fixed the bug the script became a silent no-op that nothing reported for four months. A patch step in a deploy pipeline should fail loudly when the thing it patches no longer exists.
Requirements
- Cloudflare Workers Paid plan ($5/month). The free plan's CPU limit is too short for emdash's email pipeline to initialize.
- A Postmark account with a verified sender signature.
- emdash v0.1.0+