All resources
Static site forms

Adding a Working Contact Form to a Google AI Studio App

AI Studio deploys a React client and a Node server to Cloud Run, so you do have a backend — here is why form handling still does not belong in it, and what to do instead.

The ShipMyForm team

· 4 min read

Short answer: you do have a server — AI Studio's Build mode generates a React client and a Node server and deploys both to Cloud Run. The contact form still does nothing because a server is not the missing piece. Storage, email delivery and spam filtering are, and none of them arrive with a container.

Full disclosure: ShipMyForm is our product. The accounting below is the same whichever form backend you pick, and the last section is how to build it yourself on Cloud Run if you would rather.

Why this is different from v0, Bolt or Lovable

With most AI site builders the diagnosis is simple: they emit static files, a static file cannot receive a POST, and the submit button was never connected to anything. We wrote that up in making an AI-generated contact form work.

AI Studio is not that. Build mode produces a React client and a Node.js server, manages the files and packages, and deploys the pair to Cloud Run with one click. You have a real HTTP backend on a real URL.

Which makes the dead form more confusing, not less. The code looks right. There is a route. It returns success. Nothing arrives.

What the generated route is usually doing

Something close to this:

js
app.post("/api/contact", (req, res) => {
  const { name, email, message } = req.body;
  if (!email || !message) return res.status(400).json({ error: "Missing fields" });
  // ...and then what?
  res.json({ ok: true });
});

It validates. It returns { ok: true }. The browser shows your success state. And the submission is a local variable that goes out of scope.

This is not a bug in the generation — there is genuinely nothing sensible to put on the next line without infrastructure the model cannot create for you. It cannot sign you up for a mail provider, authenticate a sending domain, or provision a database.

The three things that are actually missing

Storage. Cloud Run containers are stateless and scale to zero, so anything written to disk disappears when the instance stops — which can be minutes after your last visitor. Keeping submissions means a real database: Cloud SQL, or Firestore, each a separate service to provision, connect, secure and pay for.

Email. Nodemailer does not send mail, it speaks SMTP to something that does. You need a provider, an API key, SPF and DKIM on your domain, and a plan for bounces — and if you skip the domain authentication your notifications land in spam, which is worse than not sending them because you believe it is working.

Spam filtering. A public endpoint gets found by bots within days of going live. Without a honeypot and rate limiting, the inbox you just wired up fills with junk and you stop reading it.

The cold-start detail nobody mentions:

Cloud Run scales to zero. The first request after an idle period waits for a container to start, so someone submitting your contact form at 2am may wait several seconds. They will not know why. Some of them double-submit, and some of them leave. An endpoint that is always warm does not have this problem, and it is the one cost of self-hosting a form that does not show up on a bill.

The mistake worth preventing

AI-generated apps blur the client/server line constantly, because the model is writing both halves in one conversation. In a React SPA, anything in the client bundle is public — not obscure, not minified away, public. A mail provider key that ends up there lets anyone send mail as you until you rotate it.

js
// If this is in a file under src/ that the client imports, it is published.
const RESEND_KEY = "re_xxxxxxxxxxxx";

Keep every key on the server side of the app, and check your deployed bundle once rather than assuming. This is the most common serious defect we see in AI-generated sites, and it is invisible in the preview.

The one-line version

If you want the form working rather than owned, point it at a form endpoint and delete the route:

jsx
<form action="https://shipmyform.com/f/YOUR_FORM_ID" method="POST">
  <input name="email" type="email" required />
  <textarea name="message" required />
  <button type="submit">Send</button>
</form>

Submissions are stored, spam-filtered and emailed to you. No database, no mail provider, no key in the bundle, and nothing to wake up.

If you are fetching rather than using a plain form — which AI Studio apps usually are — the same endpoint takes JSON:

js
await fetch("https://shipmyform.com/f/YOUR_FORM_ID", {
  method: "POST",
  headers: { "Content-Type": "application/json", Accept: "application/json" },
  body: JSON.stringify({ email, message }),
});

Note what is not in that snippet: no API key. The endpoint id is safe to publish — it only accepts submissions, which is the one thing you want strangers doing.

If you would rather build it on Cloud Run

Entirely reasonable, and you are closer than most people are. The honest list:

  1. Provision Cloud SQL or Firestore and connect it from the service.
  2. Add a mail provider, authenticate your sending domain with SPF and DKIM, and keep the key in the server environment.
  3. Add a honeypot field and rate limiting per IP.
  4. Decide what happens when the mail provider is down — retry, or lose it.
  5. Set a minimum instance to avoid cold starts on the submit path, which ends Cloud Run's scale-to-zero pricing.

Step five is the one that surprises people: the moment you care about form latency, the free tier stops being free.

Next steps

Frequently asked questions

Does a Google AI Studio app have a backend?
Yes. AI Studio's Build mode generates a React client and a Node.js server and deploys both to Cloud Run, so unlike v0, Bolt or Lovable you are not working around a missing server. The reason a generated contact form still does nothing is different: the server exists but has no database to store a submission in, no mail provider to notify you with, and no spam filtering, and none of those appear just because a server does.
Why does my AI Studio contact form not send anything?
Because sending email needs a provider account, an API key and an authenticated domain, and generated code cannot create those for you. The form usually posts to a route that validates the input and returns success without delivering anywhere — which looks identical to working until you check your inbox.
Can I just store submissions in the Cloud Run container?
No. Cloud Run containers are stateless and scale to zero, so anything written to the filesystem disappears when the instance stops, which can be minutes after your last visitor. Persisting submissions means attaching a real database such as Cloud SQL or Firestore, which is a separate service with its own setup and cost.
Will a cold start lose my form submissions?
It will not lose them, but it will delay them. Cloud Run scales to zero by default, so the first request after an idle period waits for a container to start. A visitor submitting a contact form at 2am may wait several seconds for a response, which is long enough that some of them give up and resubmit. A form endpoint that is always warm does not have this problem.
Is it safe to put my API keys in an AI Studio app?
Only in the server, never in the client. Anything in the React bundle is readable by every visitor — a mail provider key in there lets anyone send mail as you until you rotate it. This is the single most common serious mistake in AI-generated apps, because generated code frequently does not distinguish between the two sides.

Related guides