lunora-setup-mail
Apps & AutomationAdds transactional email to a Lunora app. Use for sending mail (verification, password reset, invites, notifications) via `lunora registry add mail`, the `sendEmail` / `queueEmail` actions, the `SEND_EMAIL` Cloudflare Email Workers binding, Resend, React email templates, and the dev mail catcher.
How to use this skill
Bring this guide into your coding agent with a prompt tailored to the tool you use.
- Open your project in Codex.
- Copy and paste the prompt below.
- Review files and risks before approving any action.
Inspect the Agent Skill at https://github.com/anolilab/lunora/blob/HEAD/packages/cli/skills/lunora-setup-mail/SKILL.md and all supporting files. Report provenance, permissions, scripts, dependencies, and security risks. Propose an exact installation into .agents/skills/lunora-setup-mail, but wait for my approval before writing files or running scripts.
Copying this prompt does not install or run the skill. Review third-party files before use.
Lunora Setup Mail
Wire transactional email into a Lunora app using the mail registry item, which
is built on @lunora/mail (a Cloudflare Email Workers transport with
header-injection-safe address handling) and exposes a sendEmail action plus a
fire-and-forget queueEmail action. In dev, every send is captured into the
Studio Mail tab instead of going out.
When to Use
- Sending app→user mail: invites, notifications, receipts.
- Delivering verification / password-reset mail from
@lunora/auth. - Using a React (
react-email) template or a hosted provider (Resend).
When Not to Use
- The project has no Lunora backend yet — use
lunora-quickstartfirst. - Mail is already installed and you just want to send — call
ctx.runAction(api.mail.sendEmail, …)orclient.action("mail/sendEmail", …).
Workflow
- Add the
mailitem. - Configure the
SEND_EMAILbinding (or a provider) andMAIL_FROM. - Regenerate types with
lunora codegen. - Send mail from a function (or the client); render a React template if needed.
Step 1: Add the item
lunora registry add mail
This:
- Adds
@lunora/mailand@lunora/servertopackage.json(runpnpm installafterwards). - Copies
lunora/mail/index.ts(thesendEmail/queueEmailactions) into your project — it is yours to edit. - Adds a
send_emailbinding (SEND_EMAIL, with adestination_addressplaceholder) towrangler.jsoncand scaffoldsMAIL_FROM(the default sender) into.dev.vars.
Step 2: Configure delivery
| Name | Where | Notes |
|---|---|---|
SEND_EMAIL | wrangler.jsonc → send_email[] | Cloudflare Email Workers binding. Single-recipient; only verified destinations. |
MAIL_FROM | var (.dev.vars / wrangler.jsonc) | Default sender address. |
RESEND_API_KEY | secret (optional) | Use a hosted provider instead — pass apiKey to createMailer in lunora/mail. |
For production with Cloudflare Email Workers, set up
Email Routing: verify a
destination address and replace the [email protected] placeholder. Prefer
Resend? Pass apiKey (or a custom transport) to createMailer in your copied
lunora/mail/index.ts.
In lunora dev (WORKER_ENV=development) the scaffold swaps in @lunora/mail's
capture transport automatically: every send — including @lunora/auth's
verification and forgot-password mail — is intercepted and surfaced in the
Studio Mail tab. Nothing leaves your machine and no provider setup is needed.
Step 3: Regenerate types
lunora codegen
The functions surface in the generated api as api.mail.sendEmail and
api.mail.queueEmail.
Step 4: Send mail
From another function
sendEmail is an action (sending is non-transactional network I/O). From a
mutation, schedule it as a follow-up so the request is not blocked:
import { mutation, v } from "@lunora/server";
import { api } from "./_generated/api";
export const inviteUser = mutation.input({ email: v.string() }).mutation(async ({ ctx, args: { email } }) => {
// ...persist the invite, then send the mail as a follow-up action
await ctx.scheduler.runAfter(0, api.mail.sendEmail, {
to: email,
subject: "You're invited",
html: "<p>Click the link to join.</p>",
});
});
From a client
await client.action("mail/sendEmail", {
to: "[email protected]",
subject: "Welcome",
text: "Thanks for signing up!",
});
With a React email template
React elements are not JSON-serializable across the RPC boundary, so the
sendEmail args take html / text. To use a react-email template, render it
where you call the mailer — edit lunora/mail/index.ts to pass react straight
into mailer.send:
import { createMailer } from "@lunora/mail";
import { env } from "cloudflare:workers";
import { WelcomeEmail } from "./emails/Welcome";
await createMailer({ apiKey: env.RESEND_API_KEY as string, from: env.MAIL_FROM as string }).send({
to: "[email protected]",
subject: "Welcome",
react: <WelcomeEmail name="Alice" />,
});
Common Pitfalls
- Expecting prod email to "just work". Dev captures into the Studio;
production needs the
SEND_EMAILbinding (a verified destination) orRESEND_API_KEY. - Calling
sendEmailas a query/mutation. It is an action — invoke it viactx.runAction/ctx.scheduler.runAfter/client.action, neverctx.db. - Using
queueEmailwithout a Queue binding. It requires a Cloudflare Queue producer binding; until you add one,@lunora/mailthrows`queue` binding is required for mailer.queue(). The item does not add the Queue for you — see themailREADME's "Queueing" section. - Passing a React element through the action args. Render it inside the
mailer (
mailer.send({ react })), not across the RPC boundary.
Checklist
-
lunora registry add mailrun,pnpm installdone. -
SEND_EMAILbinding configured (verified destination) orRESEND_API_KEYset;MAIL_FROMset. -
lunora codegenrun soapi.mail.*is generated. - Mail sent from a function (
ctx.scheduler.runAfter/ctx.runActionwithapi.mail.sendEmail) or the client (client.action("mail/sendEmail", …)). - Verified the send appears in the Studio Mail tab in dev.