Add email sending to a Lovable app

Lovable builds a Vite frontend, so anything you put in frontend code ships to every visitor who opens the page. An API key belongs in a Lovable secret and the send belongs in backend code, which is what the request below asks for. Send from onboarding@emails.sh until your own domain is verified.

Paste into the Lovable chat

Copy this. The page it names is served as markdown at that exact URL, so the assistant reads the real integration rather than guessing at an API shape.

Paste into the Lovable chat
Add transactional email to this app using emails.sh, so a new signup gets a verification email.

Read https://emails.sh/docs.md first so you use the real request shape instead of guessing it.

Requirements:
- Do the send in backend code (an edge function), never in a React component. The API key must not appear in any file that ships to the browser, and must not use the VITE_ prefix.
- Read the key from a secret named EMAILSSH_API_KEY. I will add it under Secrets in the project's Cloud panel. If it is not set, fail with a clear error message rather than sending without it.
- Call POST https://emails.sh/v1/emails with an Authorization: Bearer header, from onboarding@emails.sh for now.
- Add a form that triggers it, and tell me exactly which secret name you expect.
01

You add the secret first.

Open the Cloud panel in your project, choose Secrets, add EMAILSSH_API_KEY with the esh_ key you created at https://emails.sh/dashboard. No VITE_ prefix: that prefix is what makes Vite embed a value into the browser bundle.

02

Lovable writes an edge function.

It creates a backend function that reads the secret, builds the JSON body, and posts to https://emails.sh/v1/emails. The key stays server-side.

03

Your frontend calls that function, not the API.

The form submits to your own function. The browser never sees an emails.sh key, only your function URL.

04

You send one real email and check the log.

The response carries an id. Look it up at https://emails.sh/dashboard to see queued, delivered, or bounced.

What Lovable writes

supabase/functions/send-verification/index.ts, the backend function behind your form.

What Lovable writes
// Runs on the server. The key is read from the project's secrets, so it is
// never part of anything the browser downloads.
Deno.serve(async (request) => {
	const apiKey = Deno.env.get('EMAILSSH_API_KEY');
	if (!apiKey) {
		// Fail loudly. A silent no-op here looks like a deliverability problem
		// three days later.
		return new Response('EMAILSSH_API_KEY is not set', { status: 500 });
	}

	const { email, token } = await request.json();
	const link = `https://acme.com/verify?token=${token}`;

	const response = await fetch('https://emails.sh/v1/emails', {
		method: 'POST',
		headers: {
			Authorization: `Bearer ${apiKey}`,
			'Content-Type': 'application/json'
		},
		body: JSON.stringify({
			from: 'Acme <onboarding@emails.sh>',
			to: [email],
			subject: 'Confirm your email',
			html: `<p><a href="${link}">Confirm your email</a></p>`,
			text: `Confirm your email: ${link}`
		})
	});

	// { "id": "...", "status": "queued" } on success. On a 4xx the body says
	// what to fix in prose, so pass it through rather than swallowing it.
	const result = await response.json();
	return new Response(JSON.stringify(result), {
		status: response.status,
		headers: { 'Content-Type': 'application/json' }
	});
});

Worth knowing

01

VITE_ is the trap

Any variable named VITE_SOMETHING is compiled into the frontend bundle and readable by anyone who opens devtools. An emails.sh key must never carry that prefix. Name it EMAILSSH_API_KEY and read it from backend code.

02

A free Lovable project can be publicly readable

That makes a hardcoded key worse than a leak in a private repo. If a key has been in frontend code at any point, revoke it in the dashboard and issue a new one.

03

Secrets do not follow you to another host

Export to Vercel, Netlify, or your own server and you set EMAILSSH_API_KEY again there, with the same name. Missing it is the usual reason the deployed version stops sending while the preview still works.

04

Ask for the docs URL by name

Without it, Lovable writes a request body it has seen elsewhere. emails.sh wants `to` as an array plus `html` and `text`, and a wrong shape comes back as a 422 rather than a silent failure.

05

It can write your lifecycle sequences as a file

An automation here is a YAML document: a trigger, an optional filter, and a list of steps that each send, wait, or branch. GET https://emails.sh/v1/automations/<id>.yaml returns it, PUT the same path replaces it, and a document you push is stored as the exact bytes you sent. So an assistant can write a trial sequence into your repository, you review the diff like any other change, and CI pushes it on merge. Errors from the parser name the wrong thing, say what to write instead, and give a line number, which is what lets an assistant correct itself. Note that this runs over the REST API rather than over MCP: there are no automation tools on the MCP server.

What arrives

One call to POST /v1/emails, and this is the message. The delivery result for it is on GET /v1/emails/:id a second later.

Sent
To:      signup@example.com
Subject: Confirm your email

Confirm your email: https://acme.com/verify?token=af31c0

Questions

Can I send straight from the React code?

No, not with a real key. Anything in the frontend bundle is public. The send has to happen in a backend function.

Do I need my own domain?

Not to start. onboarding@emails.sh works from the first send. Verify your domain when you want mail to come from your brand, which is two DNS records.

What does it cost while I am building?

The free tier is 3,000 emails a month and 100 a day, with no credit card. A project in development rarely approaches that.

My form works in preview but not on the live site.

The secret is almost always missing on the deployed environment. Set EMAILSSH_API_KEY where the live version runs, then send again.