Skip to content

SOP-WL-01: Pre-Launch Waitlist System

Document Status: PRODUCTION READY Version: 1.0 Last Updated: 2026-03-12 Owner: Operations Classification: Internal Reference


1. Overview

1.1 Purpose

Capture email signups from pre-launch visitors via nine touchpoints across the site and sync to Customer.io for launch activation.

1.2 Scope

This SOP covers the waitlist-signup Edge Function, waitlist_signups database table, Shopify theme integration, Customer.io segment, and launch activation plan.

1.3 Architecture

Three capture points feed a single Edge Function, which writes to both Supabase and Customer.io:

Browser Form (3 sources)
waitlist-signup Edge Function
       ├──▶ (1) raw_ops.waitlist_signups — upsert
       ├──▶ (2) Customer.io PUT /identify — create/update profile
       └──▶ (3) Customer.io POST /track — waitlist_signup event

1.4 Capture Points

Source Location Trigger
calculator calculator-checkout-modal.liquid (Step 1, pre-launch mode) Email entered in modal
homepage_hero waitlist-hero-cta.liquid (pre-launch mode) Email entered below hero
homepage_mid waitlist-mid-cta.liquid (pre-launch mode) Email entered after Proof section
homepage_footer final-cta.liquid (pre-launch mode) Email entered in founder statement CTA
journal article-cta.liquid (pre-launch mode) Email entered on journal article
ingredients ingredients-page.liquid CTA section (pre-launch mode) Email entered on Ingredients page
nutrition nutrition-page.liquid CTA section (pre-launch mode) Email entered on Nutrition page
how_it_works how-it-works-page.liquid CTA section (pre-launch mode) Email entered on How It Works page
about about-page.liquid CTA section (pre-launch mode) Email entered on About page

2. Database Schema

2.1 Table: raw_ops.waitlist_signups

CREATE TABLE raw_ops.waitlist_signups (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL,
  postcode TEXT,
  postcode_area TEXT,
  dog_name TEXT,
  box_size TEXT,
  daily_grams NUMERIC,
  delivery_weeks INTEGER,
  pet_count INTEGER,
  dog_weight_kg NUMERIC,
  weekly_price NUMERIC,
  source TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  confirmation_sent_at TIMESTAMPTZ,
  notified_at TIMESTAMPTZ,
  converted_at TIMESTAMPTZ
);

-- Unique constraint on email (upsert target)
CREATE UNIQUE INDEX idx_waitlist_signups_email
  ON raw_ops.waitlist_signups(email);

2.2 View: raw_ops.v_waitlist_stats

Analytics view for signup tracking.

-- Signup counts by source
SELECT source, COUNT(*) as signups
FROM raw_ops.waitlist_signups
GROUP BY source;

-- Daily signup trend
SELECT DATE(created_at) as date, COUNT(*) as signups
FROM raw_ops.waitlist_signups
GROUP BY DATE(created_at)
ORDER BY date DESC;

3. Edge Function: waitlist-signup

3.1 Location

Supabase Edge Functions: supabase/functions/waitlist-signup/index.ts

3.2 Environment Variables

Variable Purpose Note
SUPABASE_URL Database connection Auto-injected
SUPABASE_SERVICE_ROLE_KEY Database auth Auto-injected
CUSTOMERIO_SITE_ID Customer.io identify auth Must read inside serve() handler
CUSTOMERIO_API_KEY Customer.io track auth Must read inside serve() handler

Important: All Deno.env.get() calls must be inside the serve() handler function, not at module scope. Supabase Edge Functions do not reliably expose env vars at module init time. This was the root cause of Customer.io silent failures during initial deployment.

3.3 Request Payload

{
  "email": "required",
  "postcode": "optional",
  "dog_name": "optional",
  "box_size": "optional",
  "daily_grams": "optional",
  "delivery_weeks": "optional",
  "pet_count": "optional",
  "dog_weight_kg": "optional",
  "weekly_price": "optional",
  "source": "optional (calculator | homepage | journal)"
}

3.4 Two Database Paths

Condition Path Method
Postcode provided RPC: fn_waitlist_signup() Extracts postcode area, upserts with area
No postcode Direct upsert Upserts on email with available fields

3.5 Customer.io Integration

Step 1: Identify (PUT)

PUT https://track.customer.io/api/v1/customers/{email}
Authorization: Basic base64(SITE_ID:API_KEY)

{
  "email": "{email}",
  "waitlist": true,
  "waitlist_source": "{source}",
  "dog_name": "{dog_name}",
  "box_size": "{box_size}",
  "daily_grams": {daily_grams},
  "weekly_price": {weekly_price},
  "created_at": {unix_timestamp}
}

Step 2: Track Event (POST)

POST https://track.customer.io/api/v1/customers/{email}/events
Authorization: Basic base64(SITE_ID:API_KEY)

{
  "name": "waitlist_signup",
  "data": {
    "source": "{source}",
    "dog_name": "{dog_name}",
    "box_size": "{box_size}",
    "weekly_price": {weekly_price}
  }
}

4. Shopify Theme Integration

4.1 Theme Setting

File: config/settings_schema.json

{
  "name": "Pre-launch Mode",
  "settings": [
    {
      "type": "checkbox",
      "id": "prelaunch_mode",
      "label": "Pre-launch mode",
      "default": true,
      "info": "When enabled, calculator modal captures waitlist signups instead of proceeding to checkout. Disable at launch to restore full purchase flow."
    }
  ]
}

Activation: Also set "prelaunch_mode": true in config/settings_data.json (schema defaults don't apply to existing themes).

4.2 Render Points

File Behaviour (pre-launch ON) Behaviour (pre-launch OFF)
snippets/calculator-checkout-modal.liquid Step 1 captures waitlist signup, shows success screen with plan summary Normal 4-step checkout flow
sections/waitlist-hero-cta.liquid Renders waitlist form on Warm Linen bg (source: homepage_hero) Renders nothing
sections/waitlist-mid-cta.liquid Renders waitlist form on dark Espresso bg (source: homepage_mid) Renders nothing
sections/final-cta.liquid Renders waitlist-form snippet (source: homepage_footer) Renders "Calculate Your Plan" button
sections/article-cta.liquid Renders waitlist-form snippet (source: journal) Renders calculator CTA (or throttle CTA)
sections/ingredients-page.liquid CTA section renders waitlist form (source: ingredients) CTA chain: Nutrition (primary) + Calculator (secondary)
sections/nutrition-page.liquid CTA section renders waitlist form (source: nutrition) CTA chain: How It Works (primary) + Batch report (secondary)
sections/how-it-works-page.liquid CTA section renders waitlist form (source: how_it_works) Calculator CTA
sections/about-page.liquid CTA section renders waitlist form (source: about) Calculator CTA

4.3 Waitlist Form Snippet

File: snippets/waitlist-form.liquid

Reusable email capture form accepting a source parameter. Features: - Inline CSS (Montserrat/Inter, Burnt Sienna #B85C3A, Cream) - Inline JS with loading states, validation, success/error handling - Calls waitlist-signup Edge Function - Mobile responsive (stacks on <480px)

4.4 Dark Background Form Styling

File: snippets/waitlist-dark-styles.liquid

Shared CSS overrides for rendering the waitlist form on dark Espresso backgrounds. Applied by wrapping the form render in a <div class="wl-dark"> container. Hides the snippet's own heading, body text, and note (the parent section provides these). Overrides input background, border, text, and placeholder colours for legibility on dark backgrounds. Used by waitlist-mid-cta.liquid and the four interior page CTA overrides (Ingredients, Nutrition, How It Works, About).


5. Customer.io Integration

5.1 Profile Attributes

Attribute Type Set By
waitlist boolean waitlist-signup Edge Function
waitlist_source string waitlist-signup Edge Function
dog_name string waitlist-signup Edge Function (if captured)
box_size string waitlist-signup Edge Function (if captured)
daily_grams number waitlist-signup Edge Function (if captured)
weekly_price number waitlist-signup Edge Function (if captured)

5.2 Event

Name: waitlist_signup Trigger: Each signup (deduplicated by Customer.io on email) Data: source, dog_name, box_size, weekly_price

5.3 Launch Campaign

Campaign: launch_announcement Segment: All profiles where waitlist = true Trigger: Manual broadcast on launch day

Variant Condition Content
A: Full plan dog_name exists Personalised with dog name, box size, daily grams, price
B: Generic dog_name is null General launch announcement with calculator link

From: Protocol Raw <hello@protocolraw.co.uk>


6. Launch Activation Checklist

When Protocol Raw is ready to go live, follow these steps in order:

# Action Where
1 Uncheck prelaunch_mode in theme settings Shopify Admin > Themes > Customize > Theme Settings > Pre-launch Mode
2 Push theme to live shopify theme push --theme 192286327159 --allow-live
3 Remove proof portal root redirect proof-page/index.ts — delete the 302 redirect block, restore servePortalHomepage()
4 Deploy proof-page Edge Function supabase functions deploy proof-page --no-verify-jwt
5 Send launch_announcement campaign Customer.io > Campaigns > launch_announcement > Send to waitlist segment
6 Monitor waitlist conversion Check v_waitlist_stats and Customer.io campaign metrics

7. Monitoring

7.1 Pre-Launch

No automated monitoring required. Use the v_waitlist_stats view for ad hoc signup tracking:

-- Total signups by source
SELECT source, COUNT(*) as signups
FROM raw_ops.waitlist_signups
GROUP BY source
ORDER BY signups DESC;

-- Daily trend
SELECT DATE(created_at) as date, source, COUNT(*) as signups
FROM raw_ops.waitlist_signups
GROUP BY DATE(created_at), source
ORDER BY date DESC;

7.2 Post-Launch

Track conversion from waitlist to paying customer:

SELECT
  COUNT(*) as total_waitlist,
  COUNT(converted_at) as converted,
  ROUND(100.0 * COUNT(converted_at) / COUNT(*), 1) as conversion_pct
FROM raw_ops.waitlist_signups;

Document Relationship
Calculator-to-Purchase System v4.1 Modal pre-launch mode documentation
SOP-PROOF-01 v1.1 Sample batch report and pre-launch redirect
Homepage Documentation v1.3 Homepage waitlist CTA and link updates
SOP-JOURNAL-01 v1.1 Journal article waitlist CTA
SOP-EMAIL-01 v1.1 Waitlist segment and launch campaign

9. Version History

Version Date Author Changes
1.1 2026-03-14 Claude Expanded capture points from 3 to 9. Added homepage hero/mid sections, interior page CTA overrides (Ingredients, Nutrition, How It Works, About). Documented .wl-dark shared dark-background styling. Updated render points table.
1.0 2026-03-12 Claude Initial specification

Document Owner: Protocol Raw Operations