How to add a waitlist to your Remix app
A complete guide to adding a production-ready waitlist to Remix using the Enlist API: action function, form with useFetcher, and position display.
Remix's action functions run on the server, making them the right place to call the Enlist API. Here's the complete integration.
1. Get an API key
Create an account and generate a key under API keys. Add to your .env:
ENLIST_API_KEY=en_live_your_key_here
WAITLIST_ID=your_waitlist_id_here2. Create the action
import { json } from '@remix-run/node';
import type { ActionFunctionArgs } from '@remix-run/node';
export async function action({ request }: ActionFunctionArgs) {
const form = await request.formData();
const email = form.get('email');
if (!email || typeof email !== 'string') {
return json({ error: 'Email is required' }, { status: 400 });
}
const url = new URL(request.url);
const utmSource = url.searchParams.get('utm_source') ?? undefined;
const res = await fetch(
`https://api.enlist.dev/v1/waitlists/${process.env.WAITLIST_ID}/signups`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ENLIST_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, utm_source: utmSource }),
},
);
if (!res.ok) {
const data = await res.json().catch(() => ({}));
return json({ error: data.error?.message ?? 'Something went wrong' }, { status: res.status });
}
const { position, total } = await res.json();
return json({ position, total });
}3. Create the form component
useFetcher keeps the user on the page, avoiding a full-page navigation on submit.
import { useFetcher } from '@remix-run/react';
type ActionData =
| { position: number; total: number; error?: never }
| { error: string; position?: never; total?: never };
export function WaitlistForm() {
const fetcher = useFetcher<ActionData>();
const data = fetcher.data;
const isLoading = fetcher.state !== 'idle';
if (data && !data.error) {
return (
<p>
You're <strong>#{data.position}</strong> of {data.total} on the waitlist.
</p>
);
}
return (
<fetcher.Form method="post" action="/waitlist">
<input
name="email"
type="email"
required
placeholder="you@example.com"
disabled={isLoading}
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Joining…' : 'Join waitlist'}
</button>
{data?.error && <p role="alert">{data.error}</p>}
</fetcher.Form>
);
}4. Use it
import { WaitlistForm } from '~/components/WaitlistForm';
export default function Index() {
return (
<main>
<h1>Coming soon</h1>
<WaitlistForm />
</main>
);
}Notes
Remix's process.env in action functions is server-only, so the key never reaches the client bundle. The useFetcher approach avoids a page navigation on submit, which gives you fine-grained control over the success and error states in React.
Re-submitting the same email returns the existing position with no duplicate signup or email.