Build a Waitlist Page in 30 Minutes, Zero Servers
A concrete 6-step path from empty folder to a live email-capture page — frontend, backend, and database — deployed worldwide on Cloudflare for $0, before a single line of the real product exists. The fastest way to test if anyone wants the thing.
The most valuable thing you can build before your product exists is a way to find out if anyone wants it. Not a landing page you'll "get to eventually" — a real, live page that captures emails from real people, so that the number of signups can tell you something true before you've sunk three months into code. The waitlist is the cheapest experiment in software, and most founders skip it because they assume standing one up is its own small project.
It isn't. Using Cloudflare Workers and Pages, the entire stack — a form, an API that validates and stores emails, and a database to put them in — is about 30 minutes of work and one deploy command away from being live worldwide, for zero dollars. Here's the exact path, step by step, so you can run it today.
The whole point of a waitlist is to fail cheap. Thirty minutes to find out nobody wants it beats three months to find out the same thing.
Step 1 — Install the tools and scaffold (5 min)
Everything runs through wrangler, Cloudflare's command-line tool. Install it, authenticate, and scaffold a project:
npm install -g wrangler
wrangler login # opens a browser to authorize your account
npm create cloudflare@latest waitlist -- --type=hello-world --ts --no-git --no-deploy
The wrangler login step pops open a browser window where you approve access — after that, the CLI acts on your account. The create command lays down a minimal TypeScript Worker in a waitlist/ folder. You now have a working (if empty) project. Total elapsed: about five minutes, most of it waiting on npm.
Step 2 — Create the database (3 min)
Cloudflare's database is D1, a managed SQLite that gives you 5GB free — orders of magnitude more than a waitlist will ever need. Create it, then bind it to your project so the Worker can reach it:
wrangler d1 create waitlist-db
That command returns a database_id. Copy it into your wrangler.jsonc under the d1_databases binding, giving the binding a name your code will use — say, waitlist_db:
{
"d1_databases": [
{
"binding": "waitlist_db",
"database_name": "waitlist-db",
"database_id": "paste-the-returned-id-here"
}
]
}
Then create the one table you need. A waitlist is genuinely just a list of unique emails with timestamps:
CREATE TABLE emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
The UNIQUE constraint on email is doing real work — it's what stops the same person registering twice, enforced by the database instead of by code you have to remember to write. See the D1 docs for the current binding syntax if anything has shifted.
Step 3 — Write the backend API (10 min)
This is the only step with meaningful logic, and it's still short. In src/index.ts, the Worker's fetch handler receives every request, and you route the ones you care about — a POST to /api/subscribe — to insert an email:
export interface Env {
waitlist_db: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (request.method === "POST" && url.pathname === "/api/subscribe") {
const { email } = await request.json<{ email: string }>();
// Validate at the boundary — this is untrusted user input.
if (!email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
return Response.json({ error: "Enter a valid email." }, { status: 400 });
}
try {
await env.waitlist_db
.prepare("INSERT INTO emails (email) VALUES (?)")
.bind(email)
.run();
return Response.json({ ok: true });
} catch (e) {
// UNIQUE constraint violation = already on the list.
return Response.json({ ok: true, message: "You're already on the list." });
}
}
return new Response("Not found", { status: 404 });
},
};
Three things are worth pausing on. First, validate the email at the boundary — this is untrusted input from the open internet, so a format check runs before anything touches the database. Second, the .prepare().bind().run() pattern uses a parameterized query, which is what keeps this safe from SQL injection; never interpolate a user's string into the SQL directly. Third, the catch block treats a duplicate as success rather than an error — someone signing up twice is a happy path, not a failure, so tell them they're already in and move on.
To make the frontend and this API coexist in one project, set the run_worker_first (or equivalent routing) config so that /api/* requests hit the Worker while everything else serves static files from your public/ folder.
Step 4 — Write the frontend (5 min)
The page is deliberately plain: an email input, a button, and a few lines of JavaScript that POST to the API you just wrote. Drop this in public/index.html:
<!DOCTYPE html>
<html>
<head><meta charset="utf-8" /><title>Join the waitlist</title></head>
<body>
<h1>Get early access</h1>
<form id="f">
<input id="email" type="email" placeholder="you@example.com" required />
<button type="submit">Join</button>
</form>
<p id="msg"></p>
<script>
f.onsubmit = async (e) => {
e.preventDefault();
const res = await fetch("/api/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: email.value }),
});
const data = await res.json();
msg.textContent = data.error || data.message || "You're on the list!";
};
</script>
</body>
</html>
Style it later. Right now it captures emails, which is the only job that matters. You are testing demand, not winning a design award — and a bare form that's live today beats a beautiful one that ships next week.
Step 5 — Test locally (2 min)
Before you put it on the internet, run it on your own machine. wrangler dev spins up the full stack locally — Worker, routing, and a local D1 — at localhost:8787:
wrangler dev
Open the page, submit a test email, then confirm it actually landed in the database:
wrangler d1 execute waitlist-db --command "SELECT * FROM emails;"
If your test email is in that output, the whole chain works: form → API → validation → database. That's the entire system proven before a single stranger has seen it.
Step 6 — Deploy to the world (3 min)
One command ships everything — frontend, backend, and database binding — to Cloudflare's global network:
wrangler deploy
Roughly two minutes later you get a public URL like https://waitlist.<your-subdomain>.workers.dev. It's reachable worldwide, served with automatic HTTPS, and it costs nothing. If you wired the project to a GitHub repo via Pages, every subsequent git push redeploys automatically — the same push-to-deploy loop you'd want for the real product, already in place for the experiment.
What this actually buys you
Add up the free-tier headroom and the constraint disappears entirely for this use case: 100,000 Worker requests per day and 100,000 D1 row writes per day. A waitlist collecting emails before launch will not come within orders of magnitude of those limits. You are, for all practical purposes, running a real piece of global infrastructure for free — and you'll only start paying if the waitlist succeeds hard enough to need it, which is the best possible problem to have.
But the real value isn't the $0 or the 30 minutes. It's the sequencing. This page lets you test whether anyone wants the thing before you build the thing — and that's the single highest-leverage move available to a solo founder. The failure mode this prevents is the expensive one: months of building followed by the discovery that the demand was never there. A live waitlist turns that from a bet into a measurement.
- Ship the waitlist first, before the product, not after. Its whole purpose is to inform whether to build at all.
- Drive a little traffic to it — a post, a comment, a Reddit thread — and watch the signup count. That number is your most honest early signal.
- Keep the stack — the same Workers-plus-D1 setup grows straight into the real product's backend when the signups justify building it.
The stack that took you 30 minutes here — a frontend, an API, and a database, live worldwide for free — is the same stack the real product will run on. You didn't build a throwaway. You built the first working version of your infrastructure and used it to answer the only question that matters before you write the product: does anyone actually want this? One wrangler deploy from now, you can start finding out.
Part of the Vibe-entrepreneurs series on solo builder infrastructure. Previously: Deploy Global Infrastructure for Free on Cloudflare. See also Oracle's Always-Free Tier: The Server Everyone Forgets and Ship a Static Site With Nothing But git push. More builder insights.