vestige/docs/launch/waitlist-setup.md
Sam Valladares a2f4e459e1 docs(launch): viral asset package + waitlist setup runbook
- VIRAL_ASSETS_2026-07-14.md: X thread, Show HN, Reddit variants, 30s demo
  script, positioning. Firewall claim cleared after the re-audit confirmed
  enforcement; honest wording (kept-from-influencing, not "deleted").
- waitlist-setup.md refreshed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:13:10 -05:00

9.6 KiB

Vestige launch waitlist — go-live runbook (Supabase + referral loop)

The /dashboard/launch page captures signups and turns every signup into a sharer: each person gets a personal referral link, sees how many friends they brought in, and (optionally) gets a confirmation email that asks them to share. This is the loop that actually goes viral for dev launches — a shareable artifact and a "friends joined from your link" counter, not a fake queue position.

Everything below is copy-paste. Steps marked [you] need your Supabase login; everything else is already wired in the code.


Architecture (why it's safe)

The browser writes signups directly to Supabase using the public anon key. The waitlist table has insert-only Row-Level Security, so the anon key can add a row but can never read or modify the table. Everything the page needs to read (your referral code, your referral count) goes through security definer RPC functions that return only a single value — the anon key still never gets SELECT on the table. No Edge Function is required for capture.


1. Create a Supabase project [you]

https://supabase.comNew project. Pick a name (e.g. vestige-waitlist), a strong DB password (save it), the region closest to you. Wait ~2 min for it to provision.

2. Paste this SQL [you] (SQL Editor → New query → Run)

This one block creates the table, the insert-only RLS, the referral code generator, and the three RPCs the page calls. Safe to re-run (idempotent).

-- ── Vestige waitlist: table + referral loop ──────────────────────────────────
create table if not exists public.waitlist (
  id            bigint generated always as identity primary key,
  email         text not null,
  source        text,
  referrer      text,                       -- document.referrer (analytics only)
  referral_code text not null,              -- THIS user's own share code
  referred_by   text,                       -- the code that brought them in
  created_at    timestamptz not null default now()
);

-- one row per email (idempotent signups; duplicates resolve to the existing row)
create unique index if not exists waitlist_email_key
  on public.waitlist (lower(email));

-- fast lookups by referral code (own code + attribution counts)
create unique index if not exists waitlist_referral_code_key
  on public.waitlist (referral_code);
create index if not exists waitlist_referred_by_idx
  on public.waitlist (referred_by);

-- lock the table down, then allow ONLY anonymous inserts.
-- (Reads happen exclusively through the security-definer RPCs below, so the
--  anon role never needs — and never gets — SELECT on this table.)
alter table public.waitlist enable row level security;

drop policy if exists "anon can insert" on public.waitlist;
create policy "anon can insert"
  on public.waitlist
  for insert
  to anon
  with check (true);

-- short, unambiguous, URL-safe referral code (no 0/O/1/I/l to avoid confusion)
create or replace function public.gen_referral_code()
returns text
language plpgsql
as $$
declare
  alphabet constant text := 'abcdefghjkmnpqrstuvwxyz23456789';
  code text;
  i int;
begin
  loop
    code := '';
    for i in 1..7 loop
      code := code || substr(alphabet, 1 + floor(random() * length(alphabet))::int, 1);
    end loop;
    -- guarantee uniqueness against existing codes
    exit when not exists (select 1 from public.waitlist where referral_code = code);
  end loop;
  return code;
end;
$$;

-- The page's single entry point. Inserts (or finds existing) and returns the
-- caller's own referral code + how many people they've referred so far.
-- security definer => runs with table owner rights, so anon never reads directly.
create or replace function public.join_waitlist(
  p_email       text,
  p_referred_by text default null,
  p_referrer    text default null
)
returns table (referral_code text, referrals int, duplicate boolean)
language plpgsql
security definer
set search_path = public
as $$
declare
  v_email text := lower(trim(p_email));
  v_code  text;
  v_ref   text := nullif(trim(coalesce(p_referred_by, '')), '');
  v_dup   boolean := false;
begin
  if v_email !~ '^[^@\s]+@[^@\s]+\.[^@\s]+$' then
    raise exception 'invalid email';
  end if;

  -- already signed up? return their existing code (idempotent, no double-count).
  select w.referral_code into v_code
    from public.waitlist w
   where lower(w.email) = v_email
   limit 1;

  if v_code is null then
    v_code := public.gen_referral_code();
    -- never let someone refer themselves; ignore a self/unknown code gracefully.
    insert into public.waitlist (email, source, referrer, referral_code, referred_by)
    values (v_email, 'vestige-launch', p_referrer, v_code,
            case when v_ref = v_code then null else v_ref end);
  else
    v_dup := true;
  end if;

  return query
    select v_code,
           (select count(*)::int from public.waitlist where referred_by = v_code),
           v_dup;
end;
$$;

-- Live referral count for a given code (drives the "N friends joined" counter).
create or replace function public.referral_count(p_code text)
returns integer
language sql
security definer
set search_path = public
as $$
  select count(*)::int from public.waitlist where referred_by = p_code;
$$;

-- Total signups (drives the social-proof count once it's >= 25).
create or replace function public.waitlist_count()
returns integer
language sql
security definer
set search_path = public
as $$
  select count(*)::int from public.waitlist;
$$;

-- expose ONLY these RPCs to the anonymous browser role
grant execute on function public.join_waitlist(text, text, text) to anon;
grant execute on function public.referral_count(text)            to anon;
grant execute on function public.waitlist_count()                to anon;

3. Set the env vars [you]

Supabase → Project Settings → API. Copy Project URL and the anon public key into apps/dashboard/.env (already created for you with the keys blank — just paste):

VITE_SUPABASE_URL=https://<project-ref>.supabase.co
VITE_SUPABASE_ANON_KEY=<anon public key>

Restart the dev server (Vite only reads env at boot). For the deployed site, set the same two vars in your build environment and rebuild pnpm --filter @vestige/dashboard build.

4. Verify

  • Open /dashboard/launch, enter an email, submit → success state shows your personal share link + Share on X / Copy buttons.
  • Supabase → Table editor → waitlist → your row is there, with a referral_code.
  • Open the page with ?ref=<that code> in a private window, sign up a 2nd email, then reload the first link — the "N friends joined" counter goes up.
  • The total count appears on the page once there are ≥ 25 signups.

This is the launch-day multiplier: the instant someone joins, they get an email with their share link and a one-tap share button. That email is what converts a passive signup into a sharer. ~10 minutes, free tier.

  1. Get a Resend API key [you]https://resend.com → API Keys → Create. (Free tier = 3,000 emails/mo, 100/day — plenty for a waitlist.) To send from your own domain, verify it under Resend → Domains; otherwise use the shared onboarding@resend.dev sender for testing.

  2. Deploy the Edge Function (code is in supabase/functions/waitlist-welcome/index.ts). With the Supabase CLI:

    supabase login                              # [you] one-time
    supabase link --project-ref <project-ref>   # [you]
    supabase secrets set RESEND_API_KEY=<your key> \
      WAITLIST_FROM="Vestige <onboarding@resend.dev>" \
      WAITLIST_PUBLIC_URL="https://samvallad33.github.io/vestige/dashboard/launch"
    supabase functions deploy waitlist-welcome --no-verify-jwt
    
  3. Trigger it on every new signup [you] — Supabase → Database → Webhooks → Create a new hook:

    • Table: waitlist, Events: Insert
    • Type: Supabase Edge Functionswaitlist-welcome
    • (HTTP headers/payload defaults are fine.)

    Now every insert fires the function, which emails the new signup their share link. No email = signups still captured silently; you lose only the auto-share nudge.

Prefer zero email infra? Skip section 5 entirely. The in-page share link + referral counter still work — the email just adds a second, asynchronous share prompt that reaches people after they close the tab.


Exporting / emailing your list on launch day

Supabase → Table editor → waitlistExport to CSV. Or in the SQL editor:

-- everyone, newest first
select email, referral_code, referred_by, created_at
from public.waitlist order by created_at desc;

-- your top sharers (reward these people on launch day)
select referred_by as code, count(*) as brought_in
from public.waitlist
where referred_by is not null
group by referred_by order by brought_in desc limit 25;

That CSV is the asset you email when July 14 arrives. The "top sharers" query is who you publicly thank / give early access first — that recognition is itself a share trigger.

Notes

  • The page still works with no env set (local-only capture) for demos.
  • Duplicate emails are idempotent: the same person always gets the same referral code back, so re-submitting never double-counts.
  • Self-referral is blocked server-side; an unknown ?ref= code is ignored gracefully (the signup still succeeds).
  • A honeypot field + email-format validation block basic spam. Because inserts go through join_waitlist(), you can later add rate limiting there if needed.