A form on a Lovable site saves nothing until something receives the lead. Lovable can do that itself today: its built-in backend is on by default and gives your app a database and server code, so you can ask Lovable to save each lead to a table and email you about it. Three things have to be right: a table visitors can add to but not read, a submit handler that shows an error when the save fails, and an alert. Below are the prompt, the SQL to check Lovable's work against, the form code, and when a form backend is the simpler route.
Key takeaways
- Lovable's built-in backend is on by default (checked October 2026) and can store your form's leads in a table that Lovable creates when you describe it in chat.
- The table needs an insert-only rule for visitors and no read access for them. Supabase now also needs an explicit grant on every new table, or the browser gets "permission denied for table".
- A submit handler that ignores the database's answer is why many Lovable forms say "thanks" and save nothing. Check the error and show it to the visitor.
- Lovable's app emails can alert you to each lead on a paid plan with a verified domain. The Resend connector is the other built-in route, billed by Resend.
- If you want Slack alerts, spam filtering and the people who leave halfway without building any of it, send the lead to a form backend from the same handler.
What Lovable gives you today
Checked October 2026 against Lovable's documentation:
- A built-in backend, on by default. A database, user accounts, file storage, edge functions (server code) and secrets, built on Supabase's open-source foundation. You can connect your own Supabase project instead, but moving between the two later is manual, so pick one early.
- A React form with a submit handler. New projects have used TanStack Start since May 13, 2026, and older ones use React with Vite (Lovable's upgrade guide). Either way, code in the form's submit handler sends the lead, not a plain HTML
actionURL. - No leads table until you ask for one. You describe the data in chat and Lovable writes the table. The Database view lets you browse rows and export them as CSV.
- App emails on paid plans. Each paid workspace includes 50,000 transactional emails a month, sent from a domain you have verified, with hourly caps (100 an hour on Pro) (Lovable's email docs, checked October 2026).
Step 1: a table visitors can add to but not read
Paste this into the project chat:
Create a table called leads with columns id (uuid, default gen_random_uuid()),
created_at (timestamptz, default now()), name (text), email (text) and message
(text, optional). Turn on row level security. Let anonymous and signed-in
visitors insert a row only when name is 1 to 200 characters and email looks like
an email address. Nobody but the service role may read, update or delete rows.
Include the grants the Data API needs for this.
Lovable shows the database change for approval before it runs it. Check that it does the same as this SQL, which we tested on Postgres with the role names Supabase uses, to confirm that visitors can insert and can't read, update or delete:
create table public.leads (
id uuid primary key default gen_random_uuid(),
created_at timestamptz not null default now(),
name text not null,
email text not null,
message text
);
-- Visitors may add rows. Nobody but the service role may read them.
grant insert on public.leads to anon, authenticated;
grant select, insert, update, delete on public.leads to service_role;
alter table public.leads enable row level security;
create policy "Visitors can add a lead"
on public.leads
for insert
to anon, authenticated
with check (
char_length(name) between 1 and 200
and email ~* '^[^@\s]+@[^@\s]+\.[^@\s]+$'
and char_length(coalesce(message, '')) <= 5000
);
The grant lines matter. Supabase stopped exposing new tables in the public schema to its Data API automatically: the default flipped for new projects on May 30, 2026 and reached all existing projects on October 30, 2026. A table without a grant answers the browser with error 42501, "permission denied for table" (Supabase changelog, checked October 2026). That applies to your own Supabase project, and the built-in backend is built on Supabase, so keep the grants either way.
Notice what's missing: there is no read grant or read rule for visitors. They can add a lead; nobody in a browser can list them. Lovable's docs call missing row level security rules the most common way app data gets exposed, so this is the line to get right.
Step 2: wire the form's submit handler
Ask Lovable to "wire the contact form to insert into leads and show an error if the insert fails", then compare its code with this:
import { useState, type FormEvent } from "react";
import { supabase } from "@/integrations/supabase/client";
type Status = "idle" | "sending" | "sent" | "error";
export function ContactForm() {
const [status, setStatus] = useState<Status>("idle");
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault(); // without this the page reloads and the lead is lost
const form = new FormData(event.currentTarget);
// Honeypot: people never see this field; simple bots fill in every field.
if (form.get("company_site")) {
setStatus("sent");
return;
}
setStatus("sending");
const { error } = await supabase.from("leads").insert({
name: String(form.get("name") ?? "").trim(),
email: String(form.get("email") ?? "").trim(),
message: String(form.get("message") ?? "").trim() || null,
}); // no .select() here: reading the row back needs a read rule visitors must not have
if (error) {
console.error("Lead not saved:", error);
setStatus("error");
return;
}
setStatus("sent");
}
if (status === "sent") {
return <p>Thanks, we got your message and will reply soon.</p>;
}
return (
<form onSubmit={handleSubmit}>
<label>
Name <input name="name" required maxLength={200} autoComplete="name" />
</label>
<label>
Email <input name="email" type="email" required autoComplete="email" />
</label>
<label>
Message <textarea name="message" maxLength={5000} />
</label>
<div aria-hidden="true" style={{ position: "absolute", left: "-9999px" }}>
<label>
Leave this empty <input name="company_site" tabIndex={-1} autoComplete="off" />
</label>
</div>
<button type="submit" disabled={status === "sending"}>
{status === "sending" ? "Sending..." : "Send"}
</button>
{status === "error" && (
<p role="alert">That didn't send. Please try again, or email us directly.</p>
)}
</form>
);
}
Lovable usually puts the database client in your project under integrations/supabase/client; adjust the import if yours sits elsewhere. Four details do the work:
event.preventDefault()stops the browser reloading the page, which throws the lead away before your code runs.- No
.select()after.insert(). Reading the row back needs read access, which visitors don't have, so the whole insert fails with "permission denied for table leads". - The error branch. Without it, a failed save still shows "thanks", and nobody finds out until a client asks why you never replied.
- The honeypot. Bots that fill every field get a fake thank-you and nothing is saved.
One limit to know: the key your app uses to reach the database is public by design (Lovable's secrets docs say the publishable key is embedded in the page and safe to be public). Safety comes from the rules above, not from hiding the key. So a bot that skips your page and calls the table directly gets past the honeypot. If that starts happening, ask Lovable to move the insert into an edge function that checks the honeypot and limits how often one visitor can send, and to remove the visitor grant from the table.
Step 3: get told about every lead
A lead in a table you never open is still a lost lead. Two built-in ways to get an alert:
- App emails (paid plan, verified domain): ask "When a row is added to leads, send an app email to you@yourdomain.com with the name, email and message, with reply-to set to the visitor's email." Lovable writes the server code that sends it.
- The Resend connector: connect your Resend API key, then ask "Use Resend and email me when a row is added to leads." Resend bills for the email, not Lovable, and you need a verified sending domain to email anyone but yourself.
If the row arrives but the email doesn't, check the Logs view in your project first, then the usual delivery causes in contact form not sending email.
Step 4: test it like a visitor
- Publish, then open the published address (your lovable.app address or your own domain) in a private window.
- Send a lead with an email address you can check.
- Open the Database view, pick
leadsand press Refresh. The view doesn't update live, so a missing row may just need a refresh. - Confirm the alert arrived.
- Send one more with an email like
test@nowhere. You should see the error message, and no row.
When a Lovable form doesn't save
| What you see | Likely cause | Fix |
|---|---|---|
| The page reloads and nothing is saved | No preventDefault() in the handler | Add it as the first line of the handler |
| "permission denied for table leads" | No grant on the table, or a .select() after the insert | Add the insert grant; remove .select() |
| "new row violates row-level security policy" | No insert rule for visitors, or the row fails its check | Add the insert rule; check the field values |
| "Thanks" appears, but no row | The handler ignores the database's error | Check error and show it |
| The row is saved, no email | Email step missing, domain not verified, or the hourly cap | Check Logs and the domain's status |
The browser console (F12, Console) shows the exact error the handler logged, which tells you which row of the table you're in.
Or send the lead to a form backend
Building the table, the alerts and the spam handling yourself is fine for one form. If you'd rather not, or you want alerts in Slack or Discord, the same handler can post the lead to a form backend instead. OllieLeads is ours: it stores each lead in the EU (Frankfurt), filters spam with a honeypot, origin checks and rate limits, and alerts you by email, Slack or Discord within a minute. Free covers 500 leads in total with no card; Pro is $29 a month for 1,000 leads a month (pricing, checked October 2026). Spam and test leads never count.
In the handler above, replace the Supabase insert with this:
// Replaces the supabase insert in handleSubmit. frm_xxxxxxxx is your form's ID.
setStatus("sending");
const res = await fetch("https://ollieleads.com/f/frm_xxxxxxxx", {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
name: form.get("name"),
email: form.get("email"),
message: form.get("message"),
_hp: form.get("company_site") ?? "", // honeypot: a filled one is marked as spam
}),
signal: AbortSignal.timeout(3000),
}).catch(() => null);
setStatus(res?.ok ? "sent" : "error");
- Browser posts are accepted only from your brand's sites. Add your lovable.app address and your own domain under Allowed sites, and add the editor preview as a test-only site, or test on the published address.
- The handler waits at most 3 seconds and shows your error message if the post fails, so a visitor is never told "thanks" for a lead that didn't arrive.
- Its script can also save the name and email as people type and alert you about the ones who leave without sending, with a short notice next to the form. See people who leave halfway.
The same pattern works on any static or AI-built site; form backends for static sites covers the other ways to connect.
FAQ
Does Lovable have a built-in form backend?
Not a form backend as such, but its built-in backend (on by default) can store leads in a table once you ask Lovable to create one and wire the form to it. Alerts, spam handling and an inbox to work through the leads are things you add yourself, with app emails, a connector or a form backend.
Why does my Lovable form say thanks but nothing is saved?
Usually the submit handler shows the thank-you message without checking whether the insert worked. Open the browser console while you send a test: "permission denied for table" means a missing grant or a .select() after the insert, and "violates row-level security policy" means a missing or failing insert rule.
Can a Lovable form send me an email for each lead?
Yes. On a paid plan with a verified domain, Lovable's app emails can send one for every new row (50,000 a month per paid workspace, checked October 2026). You can also connect Resend, which bills you for the sending.
Is it safe that my Lovable app's database key is in the browser?
Yes for the publishable key: Lovable's docs say it's built into the page and safe to be public. What protects your leads is the table's rules: visitors can insert and can't read. The service role key is different. It stays in Lovable's secrets and must never appear in frontend code.
Can I use a Tally form in Lovable instead?
Yes. Lovable's Tally connector lets your app read your Tally forms and their submissions (Tally's word) through Tally's API, but the leads stay in Tally. For real-time updates, Lovable's docs suggest pointing Tally's webhooks at an endpoint in your app.
Want pages like this for your site? Try SuperDuperSEO.