Skip to content
Back to Vibe coding

AI app builder

Updated

Add email to your Lovable app with Lumail

Lovable apps are React front ends backed by Supabase. The browser cannot hold an email API key, so the send has to happen in a Supabase Edge Function. Ask for exactly that, and give the function the key as a secret.

TL;DR

Paste the prompt below into Lovable. It asks for a Supabase Edge Function named send-welcome that reads LUMAIL_API_KEY from function secrets, gets the signed-in user with supabase.auth.getUser(), and POSTs to https://lumail.io/api/v2/emails with an Idempotency-Key. The React app calls it with supabase.functions.invoke.

1. Prompt Lovable

Connect Supabase to your Lovable project first, then paste this into the chat. It pins the send to an Edge Function and to the signed-in user's own address, so nobody can use your function to email strangers.

Prompt for Lovable
Add transactional email to this app with Lumail (https://lumail.io). Stack: Lovable (React + Vite front end) with Supabase. Implement the send as a Supabase Edge Function called send-welcome, written for Deno, using fetch against the REST API. Rules: - Send email only from server-side code. Never call Lumail from the browser and never expose the API key to client code. - Read the API key from the LUMAIL_API_KEY environment variable. Store it as a Supabase Edge Function secret and ask me to enter the value; never put it in front-end code or a VITE_ variable. Throw a clear error if it is missing. - In TypeScript/JavaScript use the official `lumail` npm package: `new Lumail({ apiKey })`, then `lumail.emails.send({ from, to, subject, html })`. - Elsewhere call the REST API: POST https://lumail.io/api/v2/emails with `Authorization: Bearer <LUMAIL_API_KEY>` and a JSON body. - Pass exactly one body format: `html`, `markdown` or `tiptap`. Add `text` as a plain-text fallback when sending html. - `to` is a single recipient. Loop or use `lumail.emails.batch` (max 100) for several people. - The SDK returns `{ data, error }` and never throws on HTTP errors. Check `error` and log `error.name` and `error.message`. - Pass an `idempotencyKey` (header `Idempotency-Key` over REST) built from the event that triggered the email, such as `welcome:<userId>`. - `from` must use a domain verified in my Lumail organization. Put it in a LUMAIL_FROM environment variable, for example `Acme <[email protected]>`. - In the function, handle CORS preflight, create a Supabase client with the request's Authorization header, call supabase.auth.getUser(), and only send to that user's email. Return 401 when there is no user. - Call the function from the app with supabase.functions.invoke('send-welcome'). First task: send a welcome email when a user signs up. Show me which files you changed and how to test it.

2. Put the API key in the right place

Create a lum_ token in Lumail under API tokens. In a Lovable project connected to Supabase, the key belongs in the Edge Function secrets, not in the project code. Lovable usually asks you to add the secret when it writes a function that needs one; you can also add it in the Supabase dashboard under the function secrets.

Add LUMAIL_FROM the same way, so the sender address is not hard-coded.

3. The code Lovable should generate

The function should look like this. It sends only to the authenticated user, uses their id in the idempotency key so a double click cannot send twice, and returns Lumail's error name to the app.

supabase/functions/send-welcome/index.ts
import { createClient } from "npm:@supabase/supabase-js@2"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders }); const apiKey = Deno.env.get("LUMAIL_API_KEY"); if (!apiKey) return Response.json({ error: "LUMAIL_API_KEY is not set" }, { status: 500, headers: corsHeaders }); const supabase = createClient( Deno.env.get("SUPABASE_URL") ?? "", Deno.env.get("SUPABASE_ANON_KEY") ?? "", { global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } }, ); const { data: { user } } = await supabase.auth.getUser(); if (!user?.email) return Response.json({ error: "Unauthorized" }, { status: 401, headers: corsHeaders }); const response = await fetch("https://lumail.io/api/v2/emails", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "Idempotency-Key": `welcome:${user.id}`, }, body: JSON.stringify({ from: Deno.env.get("LUMAIL_FROM"), to: user.email, subject: "Welcome aboard", markdown: "Thanks for signing up. Reply to this email if you need anything.", }), }); const body = await response.json(); return Response.json(body, { status: response.status, headers: corsHeaders }); });
src/lib/send-welcome.ts
import { supabase } from "@/integrations/supabase/client"; export async function sendWelcome() { const { data, error } = await supabase.functions.invoke("send-welcome"); if (error) console.error("Welcome email failed", error); return data; }

4. Lumail MCP server and agent plugins

Lovable builds and hosts your app in the cloud, so your app talks to Lumail through the API with a token, not through MCP. The Lumail MCP server is for agents that run where you work: Claude Code, Codex, ChatGPT, Cursor or the Lumail CLI.

That split is useful once the app is live. Ask Claude Code or ChatGPT to check your domain's DNS status, look up a subscriber who did not get their email, or draft the next newsletter, while the app keeps sending through the API.

5. Verify your sending domain

Lumail only sends from a domain you have verified. Add the domain in your organization's Domains settings, then publish the SPF, DKIM and DMARC records it shows at your DNS provider. Until the domain verifies, every send fails with an error saying the domain is not authorized or verified.

Use a subdomain such as mail.yourdomain.com if your root domain already sends from another provider. Start DMARC at p=none, then tighten it once reports look clean.

Common pitfalls

  • An open relay. A function that sends to whatever to the browser posts lets anyone email anyone from your domain. Send to the authenticated user, or to a fixed address you own.
  • Missing CORS handling. Without the OPTIONS branch and CORS headers, supabase.functions.invoke fails in the browser even though the function works from curl.
  • Import path of the Supabase client. The client path in the second snippet is where Lovable projects commonly keep it; use whatever path your project already imports.
  • Client-side sends. If the AI imports lumail in a React component or uses fetch to the Lumail API from the browser, the key ships to every visitor. Move it to a server route or function and rotate the token.
  • Unverified `from` domain. Sends from a domain that is not verified in the same organization are rejected with a 400 that names the domain. Verify it first, or use the exact address Lumail shows you.
  • Treating `{ error }` as an exception. The SDK never throws on HTTP errors. Code that only wraps the call in try/catch silently drops failures.
  • Duplicate emails on retry. Without an idempotency key, a retried request or double-clicked button can send twice.

Frequently asked questions

Can a Lovable app send email without a backend?

Not safely. Any key in the React app is visible to every visitor. Use a Supabase Edge Function, which runs on the server and reads the key from its secrets.

Where do I put the Lumail API key in Lovable?

As a Supabase Edge Function secret named LUMAIL_API_KEY. Lovable typically prompts for it when it creates a function that needs it. Never store it in a VITE_ variable.

Why use fetch instead of the lumail npm package?

Edge Functions run on Deno, and a single fetch call to POST https://lumail.io/api/v2/emails needs no dependency. The request body is the same as the SDK's send parameters.

Can I use the Lumail MCP server inside Lovable?

Lovable is a cloud builder, so connect the MCP server to a local agent such as Claude Code, Codex or ChatGPT instead. Your Lovable app keeps sending through the API.

Does Supabase Auth send its emails through Lumail too?

Not by default. Supabase Auth sends confirmation and reset emails through its own mailer unless you configure custom SMTP. Lumail's SMTP relay at smtp.lumail.io on port 587 accepts a lum_ token as the password.

Keep building

Ship email from your Lovable app today.

3,000 emails a month free. Transactional and marketing email on one verified domain, with unlimited subscribers on every plan.