Capture real launch intent, not just emails.
A drop-in waitlist that asks why people want in — use case, the feature they'd actually pay for, and how much. Abuse-hardened, privacy-friendly, and stored in your own database.
One row, captured
Every signup is a structured, deduplicated row you own — ready to read, export, or act on.
Try it live
This form is the real product.
Fill it in and your answer is stored the moment you submit — the same hardened endpoint you'd ship. No payment is taken and no email is sent; it only records intent.
Submit twice with the same email and it updates your one row — it never creates a duplicate.
Drop it in
Three files. Your database. Done.
WaitlistKit is the same hardened handler this page runs on. Point it at your D1, wire the endpoint, and call the client from your form.
1 · Database
# 1. Create your D1 database, paste the id into wrangler.toml wrangler d1 create yourapp_waitlist # 2. Apply the generated schema (local for dev, --remote on deploy) wrangler d1 migrations apply yourapp_waitlist --local
2 · Endpoint
// src/routes/api/waitlist/+server.ts
import { createWaitlistHandler, methodNotAllowed } from "@micro/waitlist/server";
import { waitlistConfig } from "$lib/waitlist-config";
const handle = createWaitlistHandler(waitlistConfig);
export const POST = (event) => handle(event);
export const fallback = () => methodNotAllowed(); // others -> 4053 · Your form
import { submitWaitlist } from "@micro/waitlist/client";
const res = await submitWaitlist({
email, useCase, desiredFeature, willingnessToPay,
});
// res.ok === true the moment the row is upserted into your D1Any framework
Works wherever you ship.
The API is a plain JSON POST — drop the snippet into your existing form, no SDK required.
<form id="wk-form">
<input type="email" name="email" placeholder="you@example.com" required>
<input type="text" name="website" style="display:none"> <!-- honeypot -->
<button type="submit">Join waitlist</button>
<p id="wk-msg"></p>
</form>
<script>
document.getElementById('wk-form').addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target;
const res = await fetch('/api/waitlist', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({ email: form.email.value, website: '' })
});
const data = await res.json();
document.getElementById('wk-msg').textContent = data.ok ? "You're on the list!" : data.error;
});
</script>Hardened by default
A public write endpoint that won't get wrecked.
A waitlist is the first server surface most launches expose. WaitlistKit ships the defenses so a script kiddie can't drain your free tier or your wallet.
Idempotent upsert
Repeat submissions update one row on a UNIQUE natural key — they can never inflate row count or burn write quota.
Content-Length guard
Oversized bodies are rejected before they are ever parsed, so a giant payload can't tie up CPU.
Strict validation + clipping
Email regex, enum whitelists, and per-field length caps bound every row before it touches the database.
Honeypot
A hidden field silently rejects the simplest bots without a single third-party script.
Salted IP hash
IPs are stored only as SHA-256(salt:ip), never raw — and only when you set a salt. No raw PII.
Rate limit + Turnstile-ready
A per-IP throttle runs before the write; drop in a Turnstile key and server-side verification switches on automatically.
Pricing
Free to launch. Pay once when you outgrow it.
Start free with 100 submissions, referrals, and CSV export. The paid tiers are one-time — no subscription. Pick one to register interest; nothing is charged.
Free
Everything you need to launch a real waitlist.
- 100 submissions
- Referrals
- CSV export
Pro
Pay once, point it at your own database, ship it as your own.
- ~1,000 submissions
- Your-own-DB webhook
- Remove branding
Scale
Higher volume for launches that go big.
- Everything in Pro
- Higher submission limits
- Priority support