Tutorial
How to add a waitlist to your SvelteKit app
A complete guide to adding a production-ready waitlist to SvelteKit using the Enlist API: server endpoint, Svelte component, and position display.
SvelteKitSvelteintegration
Here's a complete SvelteKit integration with the Enlist API. The API key stays in $env/static/private, never leaving the server.
1. Get an API key
Create an account and generate a key under API keys. Add it to your .env:
ENLIST_API_KEY=en_live_your_key_here
WAITLIST_ID=your_waitlist_id_here2. Create the server endpoint
import { json, error } from '@sveltejs/kit';
import { ENLIST_API_KEY, WAITLIST_ID } from '$env/static/private';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request, url }) => {
const body = await request.json().catch(() => null);
if (!body?.email) {
throw error(400, 'Email is required');
}
const utmSource = url.searchParams.get('utm_source') ?? undefined;
const res = await fetch(`https://api.enlist.dev/v1/waitlists/${WAITLIST_ID}/signups`, {
method: 'POST',
headers: {
Authorization: `Bearer ${ENLIST_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: body.email, utm_source: utmSource }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw error(res.status, data.error?.message ?? 'Something went wrong');
}
const { position, total } = await res.json();
return json({ position, total });
};3. Create the Svelte component
<script lang="ts">
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; position: number; total: number }
| { status: 'error'; message: string };
let state: State = { status: 'idle' };
async function handleSubmit(e: SubmitEvent) {
e.preventDefault();
const email = new FormData(e.target as HTMLFormElement).get('email') as string;
state = { status: 'loading' };
try {
const res = await fetch('/api/waitlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
if (!res.ok) {
state = { status: 'error', message: data.message ?? 'Something went wrong' };
return;
}
state = { status: 'success', position: data.position, total: data.total };
} catch {
state = { status: 'error', message: 'Network error. Please try again.' };
}
}
</script>
{#if state.status === 'success'}
<p>You're <strong>#{state.position}</strong> of {state.total} on the waitlist.</p>
{:else}
<form on:submit={handleSubmit}>
<input
name="email"
type="email"
required
placeholder="you@example.com"
disabled={state.status === 'loading'}
/>
<button type="submit" disabled={state.status === 'loading'}>
{state.status === 'loading' ? 'Joining…' : 'Join waitlist'}
</button>
{#if state.status === 'error'}
<p role="alert">{state.message}</p>
{/if}
</form>
{/if}4. Use it
<script>
import WaitlistForm from '$lib/WaitlistForm.svelte';
</script>
<h1>Coming soon</h1>
<WaitlistForm />What happens on signup
- Position assigned server-side, atomically. No race conditions under concurrent load.
- Re-submitting the same email returns the original position with no duplicate email.
- SvelteKit's
$env/static/privateensures the key is never bundled into the client.
The form component handles loading, error, and success states explicitly so the UI stays correct even on network failure.