Send emails with one call. Six providers, React templates, and a local inbox you can read in your browser.
@zap-ts/mail gives the app one way to send an email. You write the body of
the email in React. You call sendEmail. The package sends it.
Six providers can do the sending: SMTP, console, Resend, Postmark, SendGrid and Brevo. A provider is the service that delivers the email. You pick one with a setting. Your code never names it.
A fresh clone sends every email to your own machine. Nothing goes to a real person.
Send an email
The package exports two things you need.
EmailLivebuilds the email client from your settings.sendEmailrenders the React body and sends it.
import { EmailLive, sendEmail } from "@zap-ts/mail";
import { Effect } from "effect";
const program = sendEmail({
to: "user@example.com",
subject: "Welcome!",
react: <p>Thanks for joining us.</p>,
});
await Effect.runPromise(program.pipe(Effect.provide(EmailLive)));
You do not pass the sender address. It comes from MAIL_FROM.
If a send fails, the package tries again, up to three times. It waits a little longer before each try. If all tries fail, it writes a warning and stops. The rest of your code keeps working. So a broken email never breaks the page the user is on.
If you need to know that an email went out, read the log. The call itself does not tell you.
Every provider is reached over HTTP. So the same code runs on Node and on Cloudflare Workers.
Templates
A template is a function that returns one ready-made email. The templates live
in @zap-ts/mail/templates. Each one returns { subject, react }. That is
what sendEmail needs, apart from the address.
import { verifyEmail } from "@zap-ts/mail/templates";
const { subject, react } = verifyEmail("https://app.example.com/api/auth/verify-email?token=1");
Six templates are about sign-in and accounts: verifyEmail,
resetPasswordEmail, deleteAccountEmail, magicLinkEmail,
signInCodeEmail and invitationEmail.
One template is about payments: lifecycleEmail. You pass it an event. The
event is trial_will_end, payment_failed or canceled. The template picks
the right body for that event.
You do not have to call these yourself. @zap-ts/authentication already sends
the sign-in emails. @zap-ts/billing already sends the payment ones.
The templates that ship are plain. They have no design and no logo. Replace them with your own. Keep the names of the exported functions, because the rest of the code imports those names.
See a template in your browser
apps/emails is a small app that shows the templates. Each file in
apps/emails/emails loads one template and draws it. You edit the real
template and see the change at once.
Start it with:
pnpm run dev:emails
It runs on port 3004.
Pick a provider
The MAIL_PROVIDER setting picks the provider when the app starts. Two of the
six need no account:
smtpsends to Mailpit. Mailpit is a fake mail server for your machine. It starts withdocker compose up. It takes mail on port 1025. You read the mail athttp://localhost:8025.consoleprints the email instead of sending it. It prints the addresses, the subject and the body. Tests use this.
The other four need a key. A key is a secret string from the provider. If the key is missing, the app fails when it starts. It does not fail later, in the middle of a send.
MAIL_PROVIDER |
What it reads |
|---|---|
smtp |
SMTP_HOST, SMTP_PORT |
console |
nothing |
resend |
RESEND_API_KEY |
postmark |
POSTMARK_SERVER_TOKEN |
sendgrid |
SENDGRID_API_KEY |
brevo |
BREVO_API_KEY |
To change provider, change one setting and add one key. You do not change any import.
Environment
These values ship in .env.schema:
MAIL_PROVIDER=smtp
MAIL_FROM=zap.ts <noreply@zapstudio.dev>
SMTP_HOST=127.0.0.1
SMTP_PORT=1025
RESEND_API_KEY, POSTMARK_SERVER_TOKEN, SENDGRID_API_KEY and
BREVO_API_KEY are there too, but they are empty. Each one has a link to the
documentation of that provider. Fill in the one you picked.
See Environment to learn how varlock turns these lines
into the typed ENV that the code reads.
Related
- Authentication — the sign-in emails
- Billing — the payment emails
- Environment — the settings the package reads
- Local development — what
docker compose upstarts