The First Email on My Waitlist Was a Question

May 10, 2026

The email landed in my inbox at some point Saturday. Subject line: “New waitlist signup: doesthisthingwork@gmail.com”. I do not know if a real person typed that address into the form, or if a friend was kind enough to pressure-test it for me, or if it is a bot that crawls form fields. I will not know for a while.

I do not really care. The point is that an email got sent. The form on the homepage talked to an API route, which talked to Resend, which talked to Gmail, which talked to me. The pipe is live.

A friend of mine builds rockets for a living. He once told me the loudest moment in a flight is not the launch — it is the first telemetry packet on the ground. Until that packet, you do not actually know if the thing you built is real. You have a strong opinion about it. You do not have a fact.

doesthisthingwork@gmail.com is my first telemetry packet.

What is on the other end of the form

Freebo’s marketing site is a Next.js app sitting at apps/website/ in the Freebo monorepo. The waitlist section is a tiny React form. It validates the email with Zod, posts to /api/waitlist, and swaps the form for a “you’re on the list” card on success. That part is boring on purpose. Forms should be boring.

The interesting code is on the server. Here is the entire route:

import { NextResponse } from "next/server";
import { Resend } from "resend";

const AUDIENCE_NAME = "Freebo Waitlist";
const NOTIFY_EMAIL = "brett.ridenour@gmail.com";

async function getOrCreateAudience(resend: Resend): Promise<string> {
  const { data: audiences } = await resend.audiences.list();
  const existing = audiences?.data?.find(
    (a: { name: string }) => a.name === AUDIENCE_NAME
  );
  if (existing) return existing.id;

  const { data: created } = await resend.audiences.create({
    name: AUDIENCE_NAME,
  });
  return created!.id;
}

export async function POST(req: Request) {
  const { email } = await req.json();
  const resend = new Resend(process.env.RESEND_API_KEY!);
  const audienceId = await getOrCreateAudience(resend);

  await resend.contacts.create({ audienceId, email, unsubscribed: false });

  await resend.emails.send({
    from: "Freebo Waitlist <onboarding@resend.dev>",
    to: NOTIFY_EMAIL,
    subject: `New waitlist signup: ${email}`,
    html: `<p>New operator joined the waitlist:</p><p><strong>${email}</strong></p>`,
  });

  return NextResponse.json({ success: true });
}

That is it. Sixty lines including the error handling I trimmed for the post. Two effects per signup: the email goes into a Resend audience called “Freebo Waitlist,” and a notification fires to my personal Gmail. The audience becomes the broadcast list. The notification becomes the dopamine.

The waitlist API route in full

Why getOrCreateAudience instead of hardcoding the ID

The first version I wrote had the audience ID baked in as a constant. Fine for one environment. Bad the moment I deploy a preview branch on Vercel and the API route in preview is talking to my real Resend account but reaching for an audience ID that exists in production only.

The getOrCreateAudience pattern is one extra round trip per request and saves me the entire category of “I deployed and now signups go to nowhere because the audience ID is stale.” It is the kind of premature defensiveness I would normally argue against. Here it bought me the freedom to never think about Resend audience IDs again, in any environment, for the life of the project. Cheap insurance.

The “from” address is a lie

If you read the route carefully, you noticed this line:

from: "Freebo Waitlist <onboarding@resend.dev>",

That is not a Freebo address. That is Resend’s shared sandbox sender. I have not finished setting up DNS for the freebo domain on Resend yet — that is on this week’s task list — so the notification is being sent from onboarding@resend.dev and it lands in my Gmail with a “via resend.dev” tag.

That is a tell. If a real operator signs up tomorrow and I send them a welcome email from that address, it lands in spam in any sane inbox. So the audience-collection half of the pipe is working. The audience-notification half is on me to finish before I can actually email people back.

I am intentionally telling you this because the gap between “the form works” and “you can actually email the people on the list” is the kind of detail that a launch announcement glosses over. A waitlist with no functioning sender domain is a list, not a channel. The list is real. The channel is half-real.

The small thing that actually matters

The waitlist section’s microcopy went through more revisions than the API route did. The current version reads like this:

You probably don’t want to switch your booking system right now. That’s fine. But if you want to watch us pick a fight with the industry giants that take 6% of every trip you sell — drop your email. Life is long. We’re going to be around for a while. And the updates will be worth it.

Three things in there I will defend.

“You probably don’t want to switch your booking system right now” is a pre-objection. The number one reason a tour operator does not sign up for a new platform is switching cost. Saying it out loud, before they think it, gives them permission to keep reading.

“Pick a fight with the industry giants” is a stake in the ground. The industry has a 6% standard take. Freebo’s pitch only works if the operator believes I am willing to make enemies. Saying it in the form copy says it before they ask the question.

“Life is long” is the line I am most proud of. The pressure on a SaaS waitlist is to be urgent. Limited beta. Founding members. Spots filling fast. None of that is true and the operator knows it. “Life is long” is the opposite move — it tells the operator that even if they are not ready today, the train keeps going. They can hop on later. The list is not a closing tactic. It is an invitation.

doesthisthingwork@gmail.com did not give me feedback on that copy. They — or the bot, or the friend — just hit submit. But the next person who hits submit might be a real charter operator in Galveston, and the copy is what they read before they decide to trust me with their email address.

The takeaway

The pipe is live. It does not deliver anything yet. There is no welcome email, no broadcast, no segmentation, no follow-up sequence. It is sixty lines of TypeScript and a single Resend audience and a notification to my Gmail.

That is the smallest possible version of “we are launching a waitlist,” and that is correct. Do not ship the welcome sequence before you ship the form. Do not write the broadcast platform before you have anyone to broadcast to. Get the first telemetry packet on the ground first. Then build outward from the fact, not from the plan.

The first packet showed up. It was a question dressed up as an email address: does this thing work?

It does.