Velocity X ships a QuickLeadForm across every service page. Click it, you start typing your email, phone, and project description. Every keystroke autosaves to Supabase via a 350ms debounced upsert. Walk away. Come back tomorrow. Your form state persists. You see the draft pre-filled. The form status is tracked (draft → submitted), which means your marketing team gets a segment of half-finished prospects to remarket to via email or retargeting pixels.
This is not a gimmick. Abandoned forms are your best lead inventory. A person who typed their email and stopped is warmer than someone who didn't visit your site at all. Capturing and remarketing to them doubles your lead pool without new ad spend.
The Problem with Traditional Forms
Standard form patterns capture data only on submit. Someone fills three fields, gets distracted, closes the tab, and the data vanishes. You lose the prospect entirely. Or they hit submit and bounce because your email confirmation was too aggressive. Either way, they're gone—no way to reach them except retargeting pixels, which are noisy and expensive.
Persistent autosave flips this. The moment someone types their first character, you own that data. Even if they never submit, you know their email, what they're interested in, and how far they got through the form. That's gold for a second touchpoint.
The Architecture
Velocity X uses a client_uuid stored in localStorage (or sessionStorage if privacy-sensitive). Every form instance generates or retrieves this UUID on mount. When the user types, a 350ms debounce timer resets and fires, sending an upsert to a Supabase table called lead_form_drafts:
// QuickLeadForm.tsx
const [clientUuid, setClientUuid] = useState<string | null>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const stored = localStorage.getItem('client_uuid');
if (!stored) {
const uuid = crypto.randomUUID();
localStorage.setItem('client_uuid', uuid);
setClientUuid(uuid);
} else {
setClientUuid(stored);
}
}, []);
const handleInputChange = async (e: React.ChangeEvent) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
if (debounceRef.current) clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(async () => {
if (clientUuid) {
await supabase
.from('lead_form_drafts')
.upsert({
client_uuid: clientUuid,
email: formData.email,
phone: formData.phone,
description: formData.description,
status: 'draft',
updated_at: new Date(),
}, { onConflict: 'client_uuid' });
}
}, 350);
};
The Supabase table uses client_uuid as the primary key. Each keystroke triggers an upsert, not an insert+update. If the row exists, only updated_at and the form fields change. No data duplication. The row's status starts as "draft" and shifts to "submitted" only when the user explicitly hits the submit button.
Why 350ms?
350 milliseconds is the sweet spot. Fast enough that it feels like the form is remembering you (no perceptible lag), slow enough that 10 keystrokes don't fire 10 Supabase requests. Anything under 200ms feels janky on slower networks. Anything over 500ms feels like the form isn't listening. 350ms is invisible and efficient.
RLS and Permissions
Supabase Row-Level Security locks down who can read the lead_form_drafts table. The client can only insert/update their own UUID. Your marketing and sales teams get a separate, authenticated read-only access via an admin role:
-- RLS policy: clients can only insert/update their own draft
CREATE POLICY "clients_write_own_drafts" ON lead_form_drafts
FOR UPDATE USING (client_uuid = auth.uid()::text);
-- RLS policy: admin/marketing can read all drafts
CREATE POLICY "admins_read_drafts" ON lead_form_drafts
FOR SELECT USING (auth.jwt() ->> 'role' = 'admin' OR auth.jwt() ->> 'role' = 'marketing');
The client doesn't authenticate (forms are public), so RLS uses client_uuid as the filter, not a user ID. The admin dashboard queries drafts with an authenticated Supabase client and sees all statuses: draft, submitted, abandoned (last_updated > 7 days ago).
Email Notification on Submit
When the form goes from "draft" → "submitted", a Supabase Edge Function listens to that change via a real-time trigger and fires off an email via Resend:
// supabase/functions/on-lead-form-submit/index.ts
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_ANON_KEY")!);
serve(async (req) => {
const { record } = await req.json();
if (record.status === 'submitted') {
const res = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: { 'Authorization': `Bearer ${Deno.env.get('RESEND_API_KEY')}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
from: 'leads@aidxn.com',
to: record.email,
subject: 'We got your enquiry.',
html: `<p>Thanks for reaching out, ${record.email}. We'll review and be in touch within 24hrs.</p>`,
}),
});
return new Response(JSON.stringify({ ok: res.ok }));
}
});
No manual polling, no batch jobs. The second the form status changes, the email fires. Simple, event-driven, reliable.
Privacy and Consent
This pattern is gold for conversions but lives in a thorny zone: you're collecting data without explicit opt-in. Velocity X handles this with a consent banner above every form: "We'll save your progress so you can finish later. No spam, ever." Checkbox opt-in for marketing email. On submit, the form respects the checkbox and only flags the row if consent was given. GDPR-compliant soft-deletion (mark is_deleted = true instead of dropping rows) lets users request removal, and your retention policy deletes truly old drafts (> 90 days, status = draft) automatically.
Remarketing Segment
Your marketing dashboard queries for drafts with status = 'draft' AND updated_at > NOW() - INTERVAL '7 days'. Export as a CSV, upload to Google Ads as a custom audience, and run a campaign: "You almost sent us your project details. Let's finish that thought." A 40–60% conversion rate on these segments is normal—they're already warm.
How Velocity Compares to Hubspot, Typeform, Formstack
Hubspot Forms autosave in Hubspot's database, but you don't own the data flow or the remarket cycle. Cost scales with submissions. Typeform and Formstack are smarter, but both charge per response and lock you into their dashboard analytics. Velocity X autosaves to your database (Supabase), owns the segment, and the marginal cost is near-zero: one Supabase request every 350ms per form instance. For 1,000 concurrent form interactions, that's 3 requests/second—well within Supabase's free tier (500K operations/month).
Frequently Asked
What if a user clears their browser cache and loses the client_uuid?
A new UUID generates, and a new draft row is created. You'll see duplicates in your database if the same person returns. Add logic to detect "same email, different UUID" and merge them, or accept mild duplication as the cost of client-side privacy (no server-side user tracking).
Can I use this for multi-step forms?
Yes. Autosave every field on every step. The step number lives in the row as current_step. On page reload, query the draft, restore the step, and let the user resume from where they left off.
How do I handle form validation on autosave?
Don't validate on autosave—save the raw values. Validate on submit. The draft might be incomplete, and that's fine. The submit handler then validates and either updates status to "submitted" or returns an error.
Should I delete old drafts automatically?
Yes. A Postgres cron job (via pg_cron) runs nightly and marks status = "deleted" for rows where updated_at < NOW() - INTERVAL '120 days'. Keep 120 days of history for remarketing windows; beyond that, the prospect is cold.
What about CCPA and international privacy laws?
Consent before autosave. Soft-delete, never hard-delete. Email should link to a data-subject-access-request flow so users can see/download/remove their draft. Supabase's RLS makes this trivial: one authenticated query returns only that user's rows.
The Bottom Line
Autosaved forms are a conversion lever most small businesses and SaaS teams ignore. A person who types halfway through your form is warmer than 99% of your cold email list. Capture them, and you've instantly created a second-touch audience without new ad spend. Velocity X bakes this pattern in: 350ms debounce, Supabase upsert, client_uuid, RLS, and a remarket segment exported to Google Ads. If transparent pricing filters leads upfront, autosaved forms catch the ones who hesitate and nurture them back. Check how Velocity X works or read how to build pre-wired OAuth dashboards for SaaS.