I launched waitinglists.dev: self-hostable waiting lists
By Flavio Copes
I built waitinglists.dev, a self-hostable double-opt-in waiting-list service I'll soon distribute with source code and agent customization instructions.
Today I launched waitinglists.dev.
It gives every project its own waiting-list endpoint.
Your site keeps the form you already designed. It sends an email address and explicit consent to waitinglists.dev. The service handles confirmation, delivery tracking, storage, removal, and exports.
The interesting part is that it is built for coding agents.
Give your agent one instruction. It can create a list, connect the form, check how many people joined, change the settings, and export the confirmed subscribers.
The agent interface is currently in private preview. Public access is not live yet.
This will be self-hostable software. I will soon distribute the complete source code, deployment instructions, and instructions written for coding agents.
You will be able to give the project to your agent and customize it in any way you want.
The form on the waitinglists.dev homepage already uses waitinglists.dev. The product is collecting its own waiting list.
Why I built it
I built a lot of products during this Summer of Code.
Almost every product needed the same thing before launch: a waiting list.
The HTML form was never the difficult part.
The difficult part was everything after someone pressed the button.
Did they clearly agree to receive updates? Did they confirm the address? Did the confirmation email arrive? What happens if it bounced? Can they remove themselves? How long should I keep an unconfirmed request? What should I export when the product is ready?
Until now I used Sendy for this.
Sendy works well for my newsletters. But creating a waiting list for every new idea became a bulky process.
I had to create another list, set up all the pieces, connect the form, check the confirmation flow, and repeat the same work for the next product.
That is a lot of setup for an idea when I do not know if people want it yet.
I could also use a form builder or a CRM.
But a waiting list does not need to become a marketing system.
I think many people will need this for the same reason.
Coding agents let us build a ton of things. But being able to build something quickly does not mean we should immediately distribute the complete product to everyone.
Sometimes we want to see if there is any demand before building further.
Sometimes the product already works, but we only want to show a small preview while we keep testing it privately.
A waiting list gives us that middle step.
We can show enough to explain the idea, see who is interested, and decide what deserves more work. The complete product can stay private until it is ready.
I wanted one quiet place for all my launches. One endpoint per product. One small dashboard. Only confirmed addresses in the final export.

Then I wanted to remove the dashboard from the normal workflow too.
Coding agents already build and change my sites. They should also be able to connect the small backend service the site needs.
That became the main idea behind waitinglists.dev:
Tell your agent. Launch your list.
You will get the source code
I am not building waitinglists.dev as a closed service you can only use through my dashboard.
I will distribute it as self-hostable software.
You will get the complete source code and the instructions needed to deploy your own copy. Your Worker, database, email configuration, domain, and subscriber data will live in your Cloudflare account.
The package will also include instructions made for coding agents.
Those instructions will explain how the project works, where the important security boundaries are, how to verify changes, and which files control each part of the product.
You can use the default version or ask your agent to change it.
For example, you could:
- redesign the dashboard around your brand
- change the confirmation flow or retention rules
- add the fields and metadata your project needs
- connect another email provider or export format
- add accounts, teams, webhooks, or other integrations
The version running at waitinglists.dev is the reference implementation.
I want the distributed version to be a finished product you can deploy immediately, not a pile of source files you must understand first.
But the source is yours to adapt.
Your frontend stays yours
waitinglists.dev does not give you a widget to restyle.
It gives you an endpoint.
A normal HTML form is enough:
<form
method="post"
action="https://waitinglists.dev/api/subscribe/my-product"
>
<input type="email" name="email" required />
<label>
<input type="checkbox" name="consent" value="yes" required />
I want updates about this product.
</label>
<button>Join the waitlist</button>
</form>
Each list has its own name, endpoint slug, and allowed browser origins.

The endpoint accepts exactly two fields: email and consent. It does not accept a name, profile, custom metadata, or anything else that might quietly turn into personal data I did not need.
After signup, the address is pending.
The person receives a confirmation email. Only confirmed subscribers appear in usable exports.
The dashboard shows confirmed and pending counts, confirmation delivery status, and failures like bounces or rejections. I can resend a failed confirmation with a fresh link.

When I am ready to launch, I can export a complete consent archive or a file prepared for Sendy, Kit, or Mailchimp.
The agent becomes the dashboard
The private preview adds a small command-line interface and JSON API.

An agent can create a waiting list and connect it to the current project with one command:
waitinglists init \
--name "My product" \
--origin "https://myproduct.com"
The command creates the list, restricts browser submissions to the product domain, and saves the public endpoint in the project.
The secret API key is stored outside the repository. The project file contains only the list ID, slug, and collection endpoint.
Exports are handled carefully too. Subscriber data is never printed in the terminal. The agent must write it to a new file, with private file permissions, and it cannot overwrite an existing export by accident.
This is a small detail, but it matters.
An agent-friendly API should not only be easy to call. It should make the unsafe action harder.
Double opt-in has a surprising problem
The basic double opt-in flow looks simple:
- someone submits an email
- the service sends a confirmation link
- the person opens the link
- the address becomes confirmed
But email security systems also open links.
They scan messages before the person reads them. If opening the URL confirms the address, a security scanner can join the waiting list on the person’s behalf.
So the confirmation link does not confirm anything.
It opens a page with a Confirm my place button. The address is confirmed only after the person presses that button and sends a POST request.
The confirmation token contains 256 random bits. I store only its keyed hash in D1. It expires after 24 hours and works once.
Repeated signups are idempotent. An already confirmed address stays confirmed, while a pending address does not receive a new message every time someone submits the form.
The public response is deliberately generic too. It does not reveal whether an address already exists.
That is a lot of work around one email input.
It is also why I wanted this service to exist.
Sending an email is not delivering an email
I used Cloudflare Email Service for confirmation messages.
The Worker sends the email directly through a restricted binding. Cloudflare manages the sending SPF, DKIM, bounce MX, and DMARC records for the domain.
Submitting a message returns an ID. It does not mean the message reached the inbox.
Delivery, deferral, bounce, rejection, complaint, and failure events arrive later through a Cloudflare Queue. The Worker consumes those events and updates the dashboard.
There is a small race hidden in that flow.
An email event can arrive before the request has finished connecting its message ID to the D1 delivery record. If the Queue consumer cannot find the message yet, it waits and retries instead of discarding the event.
There is another useful reconciliation rule.
If someone confirms through the emailed link while delivery tracking still says processing or deferred, the confirmation is stronger evidence. The dashboard treats the message as delivered.
Real systems often have two sources telling slightly different stories. The code needs to decide which evidence wins.
Minimal data by design
waitinglists.dev stores the email address, list relationship, confirmation status, consent version, and request and confirmation timestamps.
At confirmation time it also stores Cloudflare’s approximate two-letter country code. It does not keep the IP address used to derive it.
It does not store subscriber names, profiles, user agents, or browsing history.
Pending requests are deleted after their confirmation link expires. Confirmed records are deleted after 24 months at the latest. Every confirmation email also includes a signed self-service removal link.
The signed link lets the person permanently delete the address without creating an account.
I wanted the privacy rules to be part of the data model and the scheduled cleanup, not a paragraph that depends on me remembering it later.
How I built it
waitinglists.dev uses the AHA Stack.
Astro renders the application on a Cloudflare Worker. HTMX handles the small form updates, while Alpine.js handles local interface state.
Cloudflare D1 stores waiting lists, subscribers, consent records, delivery events, admin sessions, and private-preview API keys.
Around the Worker:
- Cloudflare Email Service sends confirmation messages
- Queues report email delivery outcomes
- edge rate-limit bindings protect login and signup endpoints
- Turnstile protects the private login
- a daily Cron Trigger removes expired data
The public collection endpoint has a deliberately small surface. Requests can be at most 2 KB, must contain exactly the two expected fields, and must come from an allowed origin. Rate limits apply to both the source IP and a keyed hash of the email address.
The private dashboard uses short-lived server-side sessions, secure HTTP-only cookies, CSRF tokens, and same-origin checks. API keys are shown once, stored only as hashes, and can be revoked.
I kept the first version single-owner on purpose.
There is no public account creation, team system, billing layer, campaign editor, or newsletter sender.
The default version stays small. If your installation needs one of those features, you can ask your agent to add it without making every other installation carry the same complexity.
How I deployed it
The complete application is one Cloudflare Worker.
The Worker handles normal HTTP requests, the daily cleanup job, and the Queue events from Email Service. D1 holds the durable state.
I first deployed it manually. Then I added a GitHub Actions deployment. A few minutes later I removed that and switched to native Cloudflare repository builds.
Now every push to main starts the production build. Cloudflare builds the Astro application and Wrangler deploys the generated Worker configuration.
That is less deployment code for me to maintain.
The custom domains, D1 database, Email Service binding, Queue consumer, rate limits, Cron Trigger, logs, and traces all live in wrangler.jsonc.
The service went from an empty repository to a working production system over July 30 and 31.
Building the pages was the fast part.
The real work was deciding what a confirmed subscriber means, what data not to keep, how failures appear, and how an agent can operate the system without leaking the list.
Join the waitlist
waitinglists.dev is running in production and already collecting its own waiting list.
Public agent access is not open yet. The CLI and API are in private preview while I use them on my own projects and tighten the workflow.
I am also preparing the self-hostable package with the complete source code, deployment guide, and agent instructions.
If you want to try it when it opens, join the list at waitinglists.dev.
You will go through the complete flow the product is built to provide.
Related posts about news: