Guide · 8 min

Send email from a Lovable app

Auth emails come from Supabase; your own emails come from an edge function. Here is both, with the settings to paste.

The short version: signup and password-reset email needs no code at all, just SMTP settings in Supabase. Everything else runs in a Supabase edge function, because a browser cannot speak SMTP and should never hold your password.

First, decide which emails you mean

Lovable apps use Supabase for auth, and the two kinds of email are set up in completely different places:

  • Signup confirmation, magic links, password resets. These come from Supabase, not from your code. You do not write anything — you paste SMTP settings into the Supabase dashboard. That is the Supabase Auth guide, and for most apps it is the only step needed.
  • Your own emails — welcome messages, order receipts, notifications. These need a few lines of code, in an edge function. That is the rest of this page.

Why it cannot live in the app itself

Lovable writes a React frontend that runs in your visitor's browser. A browser cannot open an SMTP connection — it has no raw TCP sockets, only HTTP — and even if it could, the password would be sitting in the JavaScript bundle for anyone to read.

So the send runs in a Supabase edge function: server-side code that ships with your project, holds the secrets, and is the one place allowed to talk to a mail server. If you ask Lovable for email without saying this, it will sometimes write browser code that silently never sends.

The four settings

Wherever the send ends up running, it needs the same four values. Create them under SMTP credentials — one per app, so you can revoke one without breaking the others.

.env
SMTP_HOST=smtp.email4vibecoder.com
SMTP_PORT=587
SMTP_USER=your-username
SMTP_PASS=your-password
MAIL_FROM="Your App <[email protected]>"

MAIL_FROM has to be on a domain you have verified in Domains. That is what lets us DKIM-sign the message; send from an unverified domain and the message is rejected rather than delivered unsigned.

The edge function

Edge functions run on Deno, so Nodemailer is not available — use denomailer, which speaks the same protocol. Create supabase/functions/send-email/index.ts:

supabase/functions/send-email/index.ts
import { SMTPClient } from 'https://deno.land/x/[email protected]/mod.ts';

Deno.serve(async (req) => {
  const { to, subject, html } = await req.json();

  const client = new SMTPClient({
    connection: {
      hostname: Deno.env.get('SMTP_HOST')!,
      port: 587,
      // false on 587: the connection starts in the clear and is upgraded
      // with STARTTLS. Use true only on port 465.
      tls: false,
      auth: {
        username: Deno.env.get('SMTP_USER')!,
        password: Deno.env.get('SMTP_PASS')!,
      },
    },
  });

  try {
    await client.send({
      from: Deno.env.get('MAIL_FROM')!,
      to,
      subject,
      html,
    });
  } finally {
    await client.close();
  }

  return new Response(JSON.stringify({ ok: true }), {
    headers: { 'Content-Type': 'application/json' },
  });
});

Set the secrets once, from your machine or the Supabase dashboard:

terminal
supabase secrets set \
  SMTP_HOST=smtp.email4vibecoder.com \
  SMTP_USER=your-username \
  SMTP_PASS=your-password \
  MAIL_FROM="Your App <[email protected]>"

supabase functions deploy send-email

Never put these in client-side code or in a variable your framework exposes to the browser. An SMTP password in a frontend bundle is readable by anyone who opens devtools, and a leaked credential gets used to send spam within hours — which costs you the credential and your domain's reputation with it.

Calling it from the app

src/lib/email.ts
const { data, error } = await supabase.functions.invoke('send-email', {
  body: {
    to: '[email protected]',
    subject: 'Welcome aboard',
    html: '<p>Thanks for signing up.</p>',
  },
});

if (error) throw error;

Leave the function's JWT verification on, which is the default. A function that sends arbitrary email to arbitrary addresses without checking who is calling is an open relay: within days it will be found and used to send spam in your name. Decide the recipient and the content from the signed-in user on the server side wherever you can, rather than trusting whatever the browser posted.

The prompt to give Lovable

Pasting this is usually faster than describing it, and it names the constraints Lovable otherwise has to guess:

prompt
Add transactional email to this app.

Send it from a Supabase edge function called send-email, using the
denomailer library (this runs on Deno, so Nodemailer will not work).
Read SMTP_HOST, SMTP_USER, SMTP_PASS and MAIL_FROM from the function's
environment — never from client code, and never hard-coded.

Use port 587 with tls: false so STARTTLS is negotiated.
Keep JWT verification enabled and derive the recipient from the
signed-in user rather than from the request body.

Then call it with supabase.functions.invoke('send-email') when a user
signs up, and surface any error instead of failing silently.

Check that it actually sent

Send one message to yourself, then open Activity. Every message we accept is listed there with the receiving server's own reply, so you can tell the difference between three very different failures: the code never ran, we rejected it, or the recipient's server did.

If it was accepted and delivered but landed in spam, that is a separate problem with a separate fix — see why emails go to spam.

Common failures

What you seeWhat it means
Nothing happens, no error in the browserThe send is probably still in client code, where it cannot work. Check that it runs in an edge function.
Cannot find module "nodemailer"Edge functions run Deno. Use denomailer, as above.
535 authentication failedSecrets not set, or set on the wrong project. Re-run the secrets command.
550 not a verified sending domainMAIL_FROM is on a domain that is not verified in Domains.