splitforms.com
← Back to the journal

How to Add a Form Backend to a v0 (Vercel) App (2026)

v0 generates a beautiful form but no endpoint to receive it. Add a working form backend in 2 minutes — one POST URL, email + webhook delivery, zero server…

Start free — 200 submissions totalSee pricing →No credit card. Paid plans from $5/mo.

Why a v0 form has no backend

v0 by Vercel is a generative UI tool. You prompt it, and it returns polished React components — usually with Tailwind and shadcn/ui — that you can drop into a Next.js app. It is excellent at the interface. What it deliberately does not do is stand up server infrastructure. A v0 "contact form" is a client component: inputs, validation, a styled submit button, and an onSubmit that, out of the box, doesn't send anywhere real.

That's by design — v0 generates UI for developers who already have (or will build) the backend. The gap shows up the moment you ship: the form looks production-ready, someone fills it out, and the submission evaporates. There was never a server route to catch it.

You have two ways to close that gap. Write the backend yourself with a Next.js Route Handler or Server Action plus an email provider — appropriate when the form drives your app's own data. Or, for the common case of a contact / waitlist / lead form whose job is "capture it and tell me," point the form at a hosted backend and skip the server code entirely. This guide does the second; if you want the first, the trade-offs are in Server Actions vs form backends.

Step 1: Get a free splitforms access key (1 minute)

splitforms is the backend we'll point the v0 form at. Free tier: 200 submissions total with dashboard storage, owner email delivery, and AI spam filtering; Starter adds webhooks — no credit card.

  1. Go to splitforms.com/login, enter your email, paste the 6-digit code
  2. Copy the access key (sf_live_...)
  3. Set your notification email under Settings → Notifications

The key is a public identifier — safe in client code. You can hardcode it or, if you prefer, expose it as a NEXT_PUBLIC_SPLITFORMS_KEY environment variable in Vercel. Both are fine; it's not a secret.

Step 2: Point the v0 form's submit at splitforms

Keep every component v0 generated. Change only the submit handler. Here it is for a typical v0 + shadcn/ui form — drop it into the existing onSubmit:

"use client";

import { useState } from "react";

const ACCESS_KEY = process.env.NEXT_PUBLIC_SPLITFORMS_KEY ?? "YOUR_ACCESS_KEY";

export function ContactForm() {
  const [status, setStatus] = useState<"idle" | "sending" | "ok" | "error">("idle");

  async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setStatus("sending");

    const data = new FormData(e.currentTarget);
    data.append("access_key", ACCESS_KEY);

    try {
      const res = await fetch("https://splitforms.com/api/submit", {
        method: "POST",
        body: data,
      });
      if (!res.ok) throw new Error();
      setStatus("ok");
      e.currentTarget.reset();
    } catch {
      setStatus("error");
    }
  }

  return (
    <form onSubmit={onSubmit}>
      {/* keep v0's generated shadcn/ui inputs and styling as-is */}
      <input name="name" required />
      <input name="email" type="email" required />
      <textarea name="message" required />

      {/* honeypot */}
      <input name="botcheck" type="checkbox" tabIndex={-1}
             autoComplete="off" style={{ display: "none" }} />

      <button type="submit" disabled={status === "sending"}>
        {status === "sending" ? "Sending..." : "Submit"}
      </button>

      {status === "ok" && <p>Thanks — we got your message.</p>}
      {status === "error" && <p>Something went wrong. Try again.</p>}
    </form>
  );
}

That's the entire backend. No app/api/contact/route.ts, no email SDK, no environment plumbing beyond the optional public key. The three things that matter: append access_key to the FormData, keep a name on every input (these become the fields in your dashboard and owner notification emails), and keep the hidden botcheck honeypot.

If your v0 form uses react-hook-form (common with shadcn/ui), submit the validated values the same way — build a FormData or post JSON; splitforms accepts both multipart/form-data and application/x-www-form-urlencoded. The pattern is identical to sending form data with fetch.

Why this beats writing a Route Handler for a contact form

You can write app/api/contact/route.ts, validate the body, call Resend or Nodemailer, handle errors, and add spam protection. For a form that's genuinely part of your product, do that. For a contact or lead form, here's what the hosted route saves you:

  • No email provider setup. No Resend account, API key, or domain verification — delivery is included with dedicated SMTP.
  • No spam code. Honeypot plus an AI classifier run server-side; you don't build or maintain them.
  • No cold-start or CORS debugging. The browser posts straight to a stable endpoint; nothing to deploy or warm up.
  • A dashboard for free. Search, filter, and export submissions without building an admin UI or querying a table.

The submission still lands as structured data you can pipe anywhere — including back into your own database via a webhook to a Vercel Route Handler if you want a copy. You get the convenience of the hosted backend without giving up control of the data.

Step 3: Test in preview and ship

Submit a test entry from v0's preview or local dev. Within ~5 seconds you should get a notification email and see the submission in your splitforms dashboard. Because the post goes directly to splitforms, it behaves the same in preview and in production — no "works locally, breaks on Vercel" surprises.

Before launch, open the splitforms security tab and add your Vercel production domain (and any custom domain) to Allowed Domains. If something doesn't arrive, check the dashboard first — present-but-no-email means a forwarding issue; absent means the POST didn't reach splitforms (verify the URL and access key in the console). Full checklist: contact form not working.

What to do next

FAQ

Why does my v0 form not submit anywhere?

v0 generates UI components — React, Tailwind, shadcn/ui. It produces the form's look and client behavior, but it doesn't create a server route to receive the submission. A generated v0 form typically has an onSubmit that does nothing, logs to the console, or shows a fake success state. To capture real submissions you need a backend: either a Next.js Route Handler / Server Action you write yourself, or a hosted form backend you POST to. The hosted route is a one-line change to the submit handler.

Should I use a Next.js Server Action or a form backend for a v0 form?

If the form feeds your app's own database and business logic, a Server Action or Route Handler is the right tool. If the form is a contact, waitlist, lead, or demo-request form whose job is 'notify me and store the submission,' a hosted form backend is less code and gives you owner email delivery, a dashboard, and spam filtering on Free, with webhooks on Starter. Many teams use both: Server Actions for in-app data, a form backend for the marketing-style forms. The detailed trade-off is in our Server Actions vs form backend comparison.

Does this work whether or not I've deployed the v0 app to Vercel?

Yes. Because the form posts directly to the splitforms endpoint from the browser, it works in v0's preview, in local development, and in production on Vercel — no API route to deploy, no environment to configure. The same handler behaves identically everywhere, which removes the usual 'works in preview, breaks in prod' class of form bugs.

Is the access key safe in a v0 / Vercel client component?

Yes. The access key is a public identifier meant to live in client code. It is not a secret and doesn't need to be an environment variable (though you can put it in NEXT_PUBLIC_ one for tidiness). Abuse is prevented server-side via rate limiting, Allowed Domains, and AI spam classification. If a key is ever abused, rotate it in the dashboard and redeploy.

Can I keep v0's shadcn/ui form components and still use this?

Yes. Keep every generated component — Input, Textarea, Button, the whole shadcn/ui form — exactly as v0 built them. You only change the submit handler to POST to splitforms. The design system, the validation, the accessibility attributes all stay. This guide's handler drops into the existing onSubmit without touching markup.

How do I send v0 form submissions to Slack or a database too?

Configure destinations once in the splitforms dashboard. Every submission delivers to email plus any signed webhooks you add — Slack, Discord, Notion, Google Sheets, or your own Vercel Route Handler if you want a copy in your database. None of that lives in your v0 app, so adding a destination later needs no redeploy.

Related articles

More practical guidance from tutorials.

Browse the journal →
Comparisons

Best Form Backends for AI Website Builders (2026)

The 10 best form backends for v0, Lovable, Bolt, and Cursor-built sites, ranked on setup spe

10 min readRead →
Tutorials

How to Add a Contact Form to a Website Built with Claude, Cursor, Lovable, v0 or Bolt

AI-generated contact forms do nothing on submit because there is no backend. Fix it with one

11 min readRead →
Tutorials

FormData in JavaScript: Submit Forms with fetch (2026 Guide)

The complete FormData guide: reading form fields, appending files, sending multipart and url

9 min readRead →

Explore this topic

Start with the overview, then move into focused guides.

OverviewWhat Is a Form Backend? (Complete Guide)Start here →GuideSelf-Hosted vs SaaS Form BackendRead →GuideForm Backend vs Form BuilderRead →GuideWhere Do Form Submissions Go?Read →ReferenceForm backend guideOpen →

Building forms with ChatGPT, Claude, Cursor, or v0? Connect the native MCP server and give your agent a production form backend.

Explore the MCP server →

Give your form a production backend.

One endpoint adds delivery, spam filtering, storage, and integrations. Start with 200 submissions total for free.

Create free accountRead the docs →
Secure checkoutSSL encryptionPrivacyProtected
VISAAMERICANEXPRESSstripe