OpenParable
Skip to content
← Back to skills

lunora-setup-mail

Apps & Automation

Adds 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.

View on GitHub ↗
QUICK START

How to use this skill

Bring this guide into your coding agent with a prompt tailored to the tool you use.

  1. Open your project in Codex.
  2. Copy and paste the prompt below.
  3. Review files and risks before approving any action.
Prompt to paste
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-quickstart first.
  • Mail is already installed and you just want to send — call ctx.runAction(api.mail.sendEmail, …) or client.action("mail/sendEmail", …).

Workflow

  1. Add the mail item.
  2. Configure the SEND_EMAIL binding (or a provider) and MAIL_FROM.
  3. Regenerate types with lunora codegen.
  4. Send mail from a function (or the client); render a React template if needed.

Step 1: Add the item

lunora registry add mail

This:

  1. Adds @lunora/mail and @lunora/server to package.json (run pnpm install afterwards).
  2. Copies lunora/mail/index.ts (the sendEmail / queueEmail actions) into your project — it is yours to edit.
  3. Adds a send_email binding (SEND_EMAIL, with a destination_address placeholder) to wrangler.jsonc and scaffolds MAIL_FROM (the default sender) into .dev.vars.

Step 2: Configure delivery

NameWhereNotes
SEND_EMAILwrangler.jsonc → send_email[]Cloudflare Email Workers binding. Single-recipient; only verified destinations.
MAIL_FROMvar (.dev.vars / wrangler.jsonc)Default sender address.
RESEND_API_KEYsecret (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

  1. Expecting prod email to "just work". Dev captures into the Studio; production needs the SEND_EMAIL binding (a verified destination) or RESEND_API_KEY.
  2. Calling sendEmail as a query/mutation. It is an action — invoke it via ctx.runAction / ctx.scheduler.runAfter / client.action, never ctx.db.
  3. Using queueEmail without a Queue binding. It requires a Cloudflare Queue producer binding; until you add one, @lunora/mail throws `queue` binding is required for mailer.queue(). The item does not add the Queue for you — see the mail README's "Queueing" section.
  4. 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 mail run, pnpm install done.
  • SEND_EMAIL binding configured (verified destination) or RESEND_API_KEY set; MAIL_FROM set.
  • lunora codegen run so api.mail.* is generated.
  • Mail sent from a function (ctx.scheduler.runAfter/ctx.runAction with api.mail.sendEmail) or the client (client.action("mail/sendEmail", …)).
  • Verified the send appears in the Studio Mail tab in dev.