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_here

2. Create the server endpoint

src/routes/api/waitlist/+server.ts
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

src/lib/WaitlistForm.svelte
<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

src/routes/+page.svelte
<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/private ensures 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.

Related posts

How to add a waitlist to your SvelteKit app · Enlist