How to create and host your own waitlist or newsletter app

Introducing a product or service starts with capturing interest—and keeping it. A small, self-hosted waitlist or newsletter app does exactly that: collect consented emails now, confirm later, and reach subscribers at launch without vendor lock-in. This guide walks through a minimal, auditable implementation patterned after waitlist.onl and its companion repositories, with Supabase for storage/auth, Resend for email, and database-native scheduling.


Core functionality (what it does)

From the user side, the flow is intentionally simple:

From the admin side, capabilities are equally focused:

Note: When automatic mode is active, the text defined in const defaultText is used for the post-launch reminder. Dashboard text is for manual sends. The automated reminder runs only for rows where reminder_sent=false.

Screenshot of a self-hosted waitlist/newsletter web app


Architecture & tech stack (deep dive)

Two reference implementations:

Key components:


Database-native scheduling (pg_cron + pg_net)

Instead of embedding schedulers in the app, Postgres runs the show:

  1. Bootstrap at first admin validation (/api/admin/validate):
    • Enable pg_cron and pg_net extensions.
    • Create a recurring cron job (e.g., every 15 minutes).
  2. Cron → HTTP:
    • The cron job calls a protected endpoint, e.g. GET /api/admin/waitlist-reminders, via pg_net.http_request.
    • A bearer token header (CRON_AUTH_TOKEN) authenticates the request.
  3. Handler logic:
    • If forceSend is provided, send to a scoped segment (manual).
    • Else, for rows with reminder_sent=false and now() >= launch_date, send one reminder, then set reminder_sent=true when updateReminderSent is true.
  4. Why this design:
    • Works reliably on serverless/containers (no long-running schedulers).
    • Auditable: scheduling lives in SQL; outbound calls are visible and monitorable.
    • Failure isolation: retries are naturally handled by the next cron tick.

This approach avoids node-cron fragility in short-lived processes and keeps reminder logic transparent.


Setup & configuration (getting started)

  1. Clone and install

    • git clone …
    • npm install
  2. Environment variables (in .env or hosting secrets)

    • SUPABASE_URL — your project URL
    • SUPABASE_ANON_KEY — public anon key for client SDKS (if used)
    • DATABASE_URL — Postgres connection string (used by server + SQL bootstrap)
    • RESEND_API_KEY — Resend API key
    • RESEND_SENDER_EMAIL — verified sender (initially often ends with @resend.dev until domain is set)
    • ADMIN_ALLOWED_EMAIL — the single email allowed to access the admin dashboard
    • CRON_AUTH_TOKEN — strong random token used as a bearer credential by pg_net when calling your reminder endpoint
    • Optional, depending on your hosting: NEXT_PUBLIC_SITE_URL, NODE_ENV, etc.
  3. Admin account

    • Create the admin user in Supabase Auth using ADMIN_ALLOWED_EMAIL.
  4. First-run validation

    • Visit /login/api/admin/validate path initializes extensions, schedules the cron job, and stores a canonical launch date if not set.
  5. Verify email

    • Configure Resend domain and DNS (SPF/DKIM) for production sending. During early testing you can use the @resend.dev sender.

Ensure you set a strong CRON_AUTH_TOKEN. Without correct env values, the app cannot reach Supabase or Resend, and reminders will not run.


Admin UX & operational notes


Lightweight telemetry (optional)

Purpose and scope:

Default behavior and controls:


Deployment notes


Recommended next steps (roadmap)


A minimal, database-scheduled waitlist/newsletter stack keeps the surface area small, the control plane clear, and your data yours. Start with the basics (clean form, consent records, one reminder), add double opt-in and unsubscribe, and grow toward templates and analytics as your audience expands. With Supabase, Resend, and Postgres-native scheduling, you can ship in an afternoon and scale with confidence.