You built a beautiful static site β maybe Astro, Next.js, or plain HTML deployed to Netlify or GitHub Pages. Everything works except one thing: forms need a server to receive submissions and send emails.
The classic solution was to spin up a backend. That means a server, a database, an email provider, rate limiting, spam filtering, and ops work β all just to receive "Hi, I want to hire you" messages.
The modern solution: use a form backend service that handles all of that. You point your form's
action (or fetch) at an endpoint, and submissions get emailed to you, stored in a
dashboard, and filtered for spam.
What you'll need: A free LaunchQ account (no credit card).
Copy your form endpoint URL from the dashboard β it looks like
https://formbox.gibby-workspace.com/submit/YOUR_FORM_ID.
The simplest possible integration. Works on any host β Netlify, Vercel, GitHub Pages, S3, Cloudflare Pages, or even a local file. No JavaScript required.
<!-- Drop this anywhere in your HTML --> <form action="https://formbox.gibby-workspace.com/submit/YOUR_FORM_ID" method="POST" > <!-- Honeypot β bots fill this, humans don't see it --> <input type="text" name="_honeypot" style="display:none" tabindex="-1" autocomplete="off"> <!-- Optional: redirect after submit --> <input type="hidden" name="_redirect" value="https://yoursite.com/thank-you"> <label for="name">Your Name</label> <input type="text" id="name" name="name" placeholder="Jane Doe" required> <label for="email">Email Address</label> <input type="email" id="email" name="email" placeholder="jane@example.com" required> <label for="message">Message</label> <textarea id="message" name="message" rows="5" required></textarea> <button type="submit">Send Message</button> </form>
π‘ How it works: The form POSTs directly to LaunchQ's endpoint. LaunchQ emails you instantly, stores the submission in your dashboard, and runs AI spam detection β then redirects the visitor to your thank-you page. No JavaScript, no API key in the frontend.
Replace YOUR_FORM_ID with the ID from your LaunchQ dashboard, and optionally set the _redirect
to a thank-you page on your site.
With React you get more control: a loading state, error handling, and a success message without a page redirect. This works with Create React App, Vite, and any React-based framework.
import { useState } from 'react';
const FORM_ID = 'YOUR_FORM_ID'; // from FormBox dashboard
const ENDPOINT = `https://formbox.gibby-workspace.com/submit/${FORM_ID}`;
export default function ContactForm() {
const [status, setStatus] = useState('idle'); // idle | loading | success | error
async function handleSubmit(e) {
e.preventDefault();
setStatus('loading');
const formData = new FormData(e.target);
try {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Accept': 'application/json' },
body: formData,
});
if (res.ok) {
setStatus('success');
e.target.reset();
} else {
setStatus('error');
}
} catch {
setStatus('error');
}
}
if (status === 'success') {
return (
<div className="success-message">
<h3>β
Message sent!</h3>
<p>Thanks β I'll get back to you within 24 hours.</p>
<button onClick={() => setStatus('idle')}>Send another</button>
</div>
);
}
return (
<form onSubmit={handleSubmit}>
{/* Honeypot */}
<input type="text" name="_honeypot" style={{ display: 'none' }} tabIndex={-1} />
<div>
<label htmlFor="name">Name</label>
<input type="text" id="name" name="name" required />
</div>
<div>
<label htmlFor="email">Email</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label htmlFor="message">Message</label>
<textarea id="message" name="message" rows={5} required />
</div>
{status === 'error' && (
<p style={{ color: 'red' }}>Something went wrong. Please try again.</p>
)}
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Sendingβ¦' : 'Send Message'}
</button>
</form>
);
}
π‘ Pass Accept: application/json in headers so LaunchQ returns JSON instead
of doing a browser redirect. That way your React component can handle the success/error state itself.
Next.js 14+ App Router supports two approaches: a classic client-side fetch, or the newer
Server Actions pattern for zero-client-JS forms.
'use client';
import { useState, FormEvent } from 'react';
const FORMBOX_ENDPOINT =
`https://formbox.gibby-workspace.com/submit/YOUR_FORM_ID`;
export default function ContactForm() {
const [state, setState] = useState<'idle'|'loading'|'done'|'error'>('idle');
async function onSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
setState('loading');
const res = await fetch(FORMBOX_ENDPOINT, {
method: 'POST',
headers: { Accept: 'application/json' },
body: new FormData(e.currentTarget),
});
setState(res.ok ? 'done' : 'error');
}
if (state === 'done')
return <p>β
Message received β I'll reply within 24 h.</p>;
return (
<form onSubmit={onSubmit} noValidate>
<input type="text" name="_honeypot" aria-hidden style={{ display: 'none' }} />
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" rows={5} required />
{state === 'error' && <p role="alert">Send failed. Try again?</p>}
<button disabled={state === 'loading'}>
{state === 'loading' ? 'Sendingβ¦' : 'Send'}
</button>
</form>
);
}
Server Actions let you handle the form on the server side, then revalidate or redirect. Useful if you want zero client-side JavaScript for the form itself.
'use server';
import { redirect } from 'next/navigation';
export async function submitContact(formData: FormData) {
// Forward directly to LaunchQ from the server
const res = await fetch(
`https://formbox.gibby-workspace.com/submit/YOUR_FORM_ID`,
{
method: 'POST',
headers: { Accept: 'application/json' },
body: formData,
}
);
if (!res.ok) {
// You can use `useFormState` for error handling
throw new Error('Submission failed');
}
redirect('/thank-you');
}
import { submitContact } from './actions';
export default function ContactPage() {
return (
<form action={submitContact}>
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" rows={5} required />
<button type="submit">Send Message</button>
</form>
);
}
β οΈ Note on Server Actions: When calling LaunchQ from a Server Action, the request comes from your Next.js server, not the user's browser. This means LaunchQ gets the server IP, not the visitor IP. For spam detection purposes, Option A (client-side fetch) gives more accurate results.
Works the same way β a fetch on submit, reactive status variable.
<script setup>
import { ref } from 'vue';
const ENDPOINT = 'https://formbox.gibby-workspace.com/submit/YOUR_FORM_ID';
const status = ref('idle'); // idle | loading | success | error
async function handleSubmit(e) {
status.value = 'loading';
const formData = new FormData(e.target);
try {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { Accept: 'application/json' },
body: formData,
});
status.value = res.ok ? 'success' : 'error';
if (res.ok) e.target.reset();
} catch {
status.value = 'error';
}
}
</script>
<template>
<div v-if="status === 'success'" class="success">
β
Message sent! I'll reply soon.
</div>
<form v-else @submit.prevent="handleSubmit">
<input type="text" name="_honeypot" aria-hidden style="display:none" />
<input type="text" name="name" placeholder="Name" required />
<input type="email" name="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" rows="5" required></textarea>
<p v-if="status === 'error'" class="error">Something went wrong. Please try again.</p>
<button :disabled="status === 'loading'">
{{ status === 'loading' ? 'Sendingβ¦' : 'Send Message' }}
</button>
</form>
</template>
Contact forms attract spam bots within hours of going live. Here's a layered defence strategy from basic to advanced.
Add a hidden field that humans never fill in. Bots auto-fill everything. If the field has a value β reject. This stops ~90% of naive bots with zero impact on real users.
<!-- Honeypot: visible to bots, hidden to humans --> <div style="position:absolute; left:-5000px;" aria-hidden="true"> <input type="text" name="_honeypot" tabindex="-1" autocomplete="off"> </div>
LaunchQ automatically checks for _honeypot in every submission β no extra code needed on your end.
LaunchQ enforces per-form rate limits out of the box. Aggressive bots that send dozens of requests get blocked automatically. You see the block count in your dashboard.
LaunchQ runs every submission through an AI content classifier that scores it on a 0-100 spam scale. Submissions above the threshold are flagged automatically β they still appear in your dashboard (in case of false positives) but won't trigger email notifications.
You can tune the threshold in your form settings:
For the most sensitive forms (login, billing enquiries), add reCAPTCHA v3 (invisible, no checkbox). Only use this if layers 1-3 aren't enough β CAPTCHA adds friction.
<!-- 1. Load the reCAPTCHA script -->
<script src="https://www.google.com/recaptcha/api.js?render=YOUR_SITE_KEY"></script>
<!-- 2. In your form submit handler -->
<script>
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const token = await grecaptcha.execute('YOUR_SITE_KEY', { action: 'contact' });
// Append token to form data
const fd = new FormData(e.target);
fd.append('g-recaptcha-response', token);
await fetch('https://formbox.gibby-workspace.com/submit/YOUR_FORM_ID', {
method: 'POST',
headers: { Accept: 'application/json' },
body: fd,
});
});
</script>
There are several services in this space. Here's how they stack up on the things that matter for indie makers and small teams.
| Feature | LaunchQ | Formspree | Netlify Forms | Basin |
|---|---|---|---|---|
| Free tier | β 100 subs/mo | β 50/mo | β 100/mo | β 100/mo |
| AI spam detection | β Yes | β No | β No | β No |
| Submission dashboard | β Full analytics | β Basic | β Basic | β Basic |
| Webhook integrations | β Yes | β Paid | β No | β Paid |
| CSV export | β Free | β Paid | β No | β Free |
| Works with any host | β Yes | β Yes | β Netlify only | β Yes |
| Email digests | β Daily + weekly | β No | β No | β No |
| Paid plan starts at | $29/mo | $16/mo | $29/mo | $12/mo |
Bottom line: If you're not locked into Netlify, LaunchQ gives you the most features on the free tier and the best spam protection at any price point.
Free tier includes 100 submissions/month, AI spam detection, and a full analytics dashboard. No credit card required.
Get Started Free βAlready have an account? Sign in β