# Waitlist Mode

Waitlist mode allows you to validate market demand before building features. Set `WAITLIST_MODE=true` and selected routes will show waitlist signup forms instead of the normal flow. Emails are captured to a database table and automatically segmented into per-blueprint Resend audiences for targeted launch campaigns.

## Use Cases

- **Pre-launch**: Gauge interest before going live
- **Feature validation**: Test new features (e.g., business plan export) before implementation
- **Market segment testing**: Validate demand for supplier tiers or new markets
- **Lean startup smoke test**: Capture leads without building full functionality

## Quick Start

### 1. Enable waitlist mode

```bash
# Add to .env
WAITLIST_MODE=true
RESEND_API_KEY=re_xyz123  # Optional: audiences created automatically per blueprint
```

### 2. Run migration (if not already done)

```bash
uv run python -m padelnomics.migrations.migrate
```

This creates the `waitlist` table with the schema:

```sql
CREATE TABLE waitlist (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT NOT NULL,
    intent TEXT NOT NULL,           -- e.g., "signup", "supplier", "free", "pro"
    source TEXT,                    -- Optional: campaign source
    plan TEXT,                      -- Optional: plan name
    ip_address TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(email, intent)           -- Same email can sign up for different intents
);
CREATE INDEX idx_waitlist_email ON waitlist(email);
```

### 3. Test it

Visit any gated route:

- http://localhost:5000/auth/signup → entrepreneur waitlist
- http://localhost:5000/suppliers/signup → supplier waitlist
- http://localhost:5000/planner/export → business plan export waitlist

Submit an email → see confirmation page → check the `waitlist` table:

```bash
uv run python -c "
import sqlite3
conn = sqlite3.connect('data/app.db')
rows = conn.execute('SELECT * FROM waitlist ORDER BY created_at DESC LIMIT 10').fetchall()
for row in rows:
    print(row)
"
```

### 4. Turn off waitlist mode

```bash
# In .env
WAITLIST_MODE=false
```

Restart the app. All routes revert to normal signup/checkout flows.

---

## Architecture

### Two Abstractions

1. **`@waitlist_gate(template, **context)` decorator** — intercepts GET requests
2. **`capture_waitlist_email(email, intent, plan, email_intent)` helper** — handles DB + email + Resend

### How It Works

#### GET Requests (Decorator)

The `@waitlist_gate` decorator intercepts GET requests when `WAITLIST_MODE=true` and renders a waitlist template instead of the normal page.

```python
from padelnomics.core import waitlist_gate

@bp.route("/signup", methods=["GET", "POST"])
@csrf_protect
@waitlist_gate("waitlist.html", plan=lambda: request.args.get("plan", "free"))
async def signup():
    """Signup page."""
    # This code only runs when WAITLIST_MODE=false or for POST requests
    ...
```

**How it works:**
- Checks `config.WAITLIST_MODE` and `request.method`
- If mode is enabled AND method is GET → renders the waitlist template
- Otherwise → passes through to the wrapped function
- Context variables can be callables (evaluated at request time) or static values

**Why POST passes through:**
- Routes need to handle waitlist form submissions
- Each route controls its own POST logic (validation, error handling, success template)

#### POST Requests (Helper)

The `capture_waitlist_email()` helper handles the database insertion, email queueing, and Resend integration.

```python
from padelnomics.core import capture_waitlist_email

async def signup():
    if config.WAITLIST_MODE and request.method == "POST":
        form = await request.form
        email = form.get("email", "").strip().lower()

        if not email or "@" not in email:
            await flash("Please enter a valid email address.", "error")
            return redirect(url_for("auth.signup"))

        await capture_waitlist_email(email, intent="signup")
        return await render_template("waitlist_confirmed.html", email=email)

    # Normal signup flow below...
```

**What it does:**
1. Inserts email into `waitlist` table with `INSERT OR IGNORE` (idempotent)
2. Returns `True` if new row inserted, `False` if duplicate
3. Enqueues `send_waitlist_confirmation` email task (only for new signups)
4. Adds email to Resend audience if `RESEND_AUDIENCE_WAITLIST` is set
5. All errors are handled silently — user always sees success page

**Parameters:**
- `email` (str) — Email address to capture
- `intent` (str) — Intent value stored in database (e.g., "signup", "supplier")
- `plan` (str, optional) — Plan name stored in database (e.g., "supplier_pro")
- `email_intent` (str, optional) — Intent value for email confirmation (defaults to `intent`)

**Why `email_intent`?**

Suppliers need different intent values for DB vs email:
- **Database**: `intent="supplier"` (all suppliers grouped together)
- **Email**: `intent="supplier_pro"` (plan-specific messaging)

```python
await capture_waitlist_email(
    email,
    intent="supplier",           # DB: all suppliers
    plan="supplier_pro",          # DB: plan name
    email_intent="supplier_pro"   # Email: plan-specific
)
```

---

## Adding Waitlist to New Routes

### Step 1: Import the abstractions

```python
from ..core import capture_waitlist_email, waitlist_gate
```

### Step 2: Add the decorator

```python
@bp.route("/your-route", methods=["GET", "POST"])
@csrf_protect
@waitlist_gate("your_waitlist_template.html", custom_var=lambda: some_value())
async def your_route():
    ...
```

### Step 3: Handle POST in your route

```python
async def your_route():
    if config.WAITLIST_MODE and request.method == "POST":
        form = await request.form
        email = form.get("email", "").strip().lower()

        # Validate email
        if not email or "@" not in email:
            await flash("Please enter a valid email address.", "error")
            return redirect(url_for("your_blueprint.your_route"))

        # Capture to waitlist
        await capture_waitlist_email(email, intent="your_intent")

        # Show confirmation
        return await render_template("your_confirmation.html", email=email)

    # Normal flow below...
    ...
```

### Step 4: Create templates

1. **Waitlist form** (`your_waitlist_template.html`)

```html
{% extends "base.html" %}

{% block content %}
<div class="max-w-md mx-auto mt-12">
  <h1 class="text-3xl font-bold mb-4">Join the Waitlist</h1>
  <p class="text-gray-600 mb-8">
    We're launching soon! Enter your email to get early access.
  </p>

  <form method="post" class="space-y-4">
    <input type="hidden" name="csrf_token" value="{{ get_csrf_token() }}">

    <div>
      <label for="email" class="block text-sm font-medium mb-2">Email</label>
      <input
        type="email"
        name="email"
        id="email"
        required
        class="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500"
        placeholder="you@example.com"
      >
    </div>

    <button
      type="submit"
      class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700"
    >
      Join Waitlist
    </button>
  </form>
</div>
{% endblock %}
```

2. **Confirmation page** (`your_confirmation.html`)

```html
{% extends "base.html" %}

{% block content %}
<div class="max-w-md mx-auto mt-12 text-center">
  <div class="mb-6">
    <svg class="w-16 h-16 mx-auto text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
    </svg>
  </div>

  <h1 class="text-3xl font-bold mb-4">You're on the list!</h1>

  <p class="text-gray-600 mb-4">
    We've sent a confirmation to <strong>{{ email }}</strong>.
  </p>

  <p class="text-gray-600">
    We'll notify you when we launch. In the meantime, follow us on social media for updates.
  </p>
</div>
{% endblock %}
```

---

## Email Confirmations

Waitlist confirmations are sent via the `send_waitlist_confirmation` worker task.

### How it works

1. Route calls `capture_waitlist_email()`
2. Helper enqueues: `await enqueue("send_waitlist_confirmation", {"email": email, "intent": intent})`
3. Worker picks up task and calls `handle_send_waitlist_confirmation()`
4. Email sent via Resend (or printed to console in dev)

### Email content

Defined in `src/padelnomics/worker.py`:

```python
async def handle_send_waitlist_confirmation(data):
    email = data["email"]
    intent = data.get("intent", "signup")

    # Intent-specific messaging
    if intent == "signup" or intent.startswith("free") or intent.startswith("starter"):
        subject = "Thanks for joining the Padelnomics waitlist!"
        body = "We're launching soon..."
    elif intent.startswith("supplier"):
        plan_name = intent.replace("supplier_", "").title()
        subject = f"You're on the list for Padelnomics {plan_name}"
        body = f"Thanks for your interest in the {plan_name} plan..."
    else:
        subject = "You're on the Padelnomics waitlist"
        body = "We'll notify you when we launch..."

    await send_email(
        to=email,
        subject=subject,
        html=body,
        from_addr=EMAIL_ADDRESSES["transactional"]
    )
```

### Customizing emails

Edit `handle_send_waitlist_confirmation()` in `worker.py` to:
- Add new intent-specific messaging
- Include plan details or pricing
- Add CTA buttons or links
- Personalize based on source/plan

---

## Resend Integration

Audiences are created automatically per blueprint — no configuration needed beyond `RESEND_API_KEY`. The first signup from each blueprint creates a named audience in Resend and caches its ID in the `resend_audiences` database table. Subsequent signups from the same blueprint skip the API call and use the cached ID.

### Audience names

| Route | Blueprint | Resend audience |
|-------|-----------|-----------------|
| `/auth/signup` | `auth` | `waitlist-auth` |
| `/suppliers/signup/waitlist` | `suppliers` | `waitlist-suppliers` |
| `/planner/export` | `planner` | `waitlist-planner` |

Adding `@waitlist_gate` to any new blueprint automatically creates its own audience on first signup.

### Setup

Set `RESEND_API_KEY` in `.env` — no audience IDs needed:

```bash
RESEND_API_KEY=re_xyz123
```

### How it works

```python
# Derives audience name from current request's blueprint
blueprint = request.blueprints[0] if request.blueprints else "default"
audience_name = f"waitlist-{blueprint}"

# Lazy-creates audience on first use, caches ID in resend_audiences table
audience_id = await _get_or_create_resend_audience(audience_name)
if audience_id:
    resend.Contacts.create({"email": email, "audience_id": audience_id})
```

**Silent failures:**
- `Audiences.create` error → `audience_id` is None, contact skipped
- `Contacts.create` error → ignored
- Duplicate emails (already in audience) → ignored
- API rate limits → ignored

Waitlist signups always succeed even if Resend is down.

### Launch campaign

When ready to launch:

1. Go to Resend dashboard → Audiences → select the segment (e.g., `waitlist-suppliers`)
2. Create a broadcast email: "We're live! Here's your early access..."
3. Send to the audience

---

## Testing

### Unit Tests

Tests are in `tests/test_waitlist.py`:

```bash
uv run pytest tests/test_waitlist.py -v
```

**Test coverage:**
- Configuration: `WAITLIST_MODE` flag exists and can be toggled
- Migration: `waitlist` table schema, constraints, indexes
- Worker: Email confirmation task content and addressing
- Auth routes: GET shows waitlist, POST captures email
- Supplier routes: Waitlist form, email capture, plan-specific messaging
- Planner routes: Export waitlist gate
- Decorator: GET intercept, POST passthrough, callable context
- Helper: DB operations, email queueing, Resend integration, error handling
- Edge cases: Duplicate emails, invalid emails, DB errors, Resend errors
- Integration: Full flows from GET → POST → DB → email

### Manual Testing

1. **Enable waitlist mode:**
   ```bash
   # In .env
   WAITLIST_MODE=true
   ```

2. **Visit gated routes:**
   - http://localhost:5000/auth/signup?plan=starter
   - http://localhost:5000/suppliers/signup?plan=supplier_pro
   - http://localhost:5000/planner/export

3. **Submit emails:**
   - Valid email → confirmation page
   - Invalid email → error message
   - Duplicate email → confirmation page (no error)

4. **Check database:**
   ```bash
   uv run python -c "
   import sqlite3
   conn = sqlite3.connect('data/app.db')
   rows = conn.execute('SELECT * FROM waitlist ORDER BY created_at DESC').fetchall()
   for row in rows:
       print(row)
   "
   ```

5. **Check worker logs:**
   - Start worker: `uv run python -m padelnomics.worker`
   - Submit email via form
   - Look for: `[TASK] Processing send_waitlist_confirmation...`
   - Email should print to console (if no Resend key) or send via Resend

6. **Disable waitlist mode:**
   ```bash
   # In .env
   WAITLIST_MODE=false
   ```
   - Restart app
   - Visit routes → should see normal signup/checkout flows

---

## Database Schema

### `waitlist` table

```sql
CREATE TABLE waitlist (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT NOT NULL,
    intent TEXT NOT NULL,
    source TEXT,
    plan TEXT,
    ip_address TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(email, intent)
);

CREATE INDEX idx_waitlist_email ON waitlist(email);
```

**Columns:**
- `id` — Auto-incrementing primary key
- `email` — Email address (lowercase, trimmed)
- `intent` — Signup intent (e.g., "signup", "supplier", "free", "pro")
- `source` — Optional campaign source (e.g., "facebook_ad", "twitter")
- `plan` — Optional plan name (e.g., "supplier_pro", "business_plan")
- `ip_address` — Request IP address (for spam detection)
- `created_at` — Timestamp (ISO 8601)

**Constraints:**
- `UNIQUE(email, intent)` — Same email can sign up for multiple intents
  - Example: `user@example.com` can be on both "signup" and "supplier" waitlists
  - Duplicate submissions for same email+intent are ignored (idempotent)

**Indexes:**
- `idx_waitlist_email` — Fast lookups by email

### Queries

**Count total signups:**
```sql
SELECT COUNT(*) FROM waitlist;
```

**Count by intent:**
```sql
SELECT intent, COUNT(*) as count
FROM waitlist
GROUP BY intent
ORDER BY count DESC;
```

**Recent signups:**
```sql
SELECT * FROM waitlist
ORDER BY created_at DESC
LIMIT 50;
```

**Duplicates (same email, different intents):**
```sql
SELECT email, COUNT(*) as count
FROM waitlist
GROUP BY email
HAVING count > 1;
```

**Export for Resend:**
```sql
SELECT DISTINCT email FROM waitlist;
```

---

## Configuration Reference

### Environment Variables

| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `WAITLIST_MODE` | bool | `false` | Enable waitlist gates on routes |
| `RESEND_API_KEY` | string | `""` | Resend API key — audiences created automatically per blueprint |

### Usage

```bash
# .env
WAITLIST_MODE=true
RESEND_API_KEY=re_xyz123
# No audience IDs needed — created automatically on first signup
```

### Config Access

```python
from padelnomics.core import config

if config.WAITLIST_MODE:
    # Show waitlist
else:
    # Normal flow
```

---

## Best Practices

### 1. Keep waitlist templates simple

- Single form field (email)
- Clear value proposition
- No extra fields (friction kills conversions)
- Mobile-friendly

### 2. Always show confirmation page

- Don't redirect to homepage
- Show success message with email address
- Set expectations (when will they hear from you?)
- Add social proof or testimonials

### 3. Test with real emails

- Use your own email to verify full flow
- Check spam folder for confirmations
- Test with different email providers (Gmail, Outlook, ProtonMail)

### 4. Monitor signups

```sql
-- Daily signups
SELECT DATE(created_at) as date, COUNT(*) as signups
FROM waitlist
GROUP BY DATE(created_at)
ORDER BY date DESC
LIMIT 30;

-- Conversion rate (if tracking sources)
SELECT source, COUNT(*) as signups
FROM waitlist
GROUP BY source
ORDER BY signups DESC;
```

### 5. Plan your launch

- Set a signup goal (e.g., "100 signups in 30 days")
- Promote waitlist via social media, ads, content
- Send weekly updates to waitlist (build excitement)
- Launch when you hit goal or deadline

### 6. Export before launch

```sql
-- Export all emails for Resend broadcast
SELECT DISTINCT email FROM waitlist;
```

Save as CSV, upload to Resend audience, send launch announcement.

### 7. Clean up after launch

```sql
-- Archive waitlist signups (optional)
CREATE TABLE waitlist_archive AS SELECT * FROM waitlist;

-- Clear table (optional - only if you want to reuse for another feature)
DELETE FROM waitlist;
```

---

## Troubleshooting

### Waitlist page not showing

**Symptoms:** Visit `/auth/signup` → see normal signup form

**Causes:**
1. `WAITLIST_MODE` not set to `true` in `.env`
2. `.env` changes not loaded (need to restart app)
3. Using wrong environment (production vs dev)

**Fix:**
```bash
# Check config
uv run python -c "from padelnomics.core import config; print(config.WAITLIST_MODE)"

# Should print: True

# If False, check .env and restart:
# WAITLIST_MODE=true
```

### Emails not captured in database

**Symptoms:** Submit form → confirmation page shows, but no row in `waitlist` table

**Causes:**
1. Database migration not run
2. DB connection error
3. Email validation rejecting input

**Fix:**
```bash
# Check if table exists
uv run python -c "
import sqlite3
conn = sqlite3.connect('data/app.db')
tables = [r[0] for r in conn.execute(\"SELECT name FROM sqlite_master WHERE type='table'\").fetchall()]
print('waitlist' in tables)
"

# If False, run migration:
uv run python -m padelnomics.migrations.migrate
```

### Confirmation emails not sending

**Symptoms:** Email captured in DB, but no confirmation email sent/printed

**Causes:**
1. Worker not running
2. Task queue error
3. Resend API error

**Fix:**
```bash
# Check worker is running
ps aux | grep "python -m padelnomics.worker"

# Start if not running
uv run python -m padelnomics.worker

# Check worker logs for errors
# Look for: [TASK] Processing send_waitlist_confirmation...
```

### Resend audience not adding contacts

**Symptoms:** Emails captured, confirmations sent, but not appearing in Resend audience

**Causes:**
1. `RESEND_API_KEY` not set
2. `Audiences.create` API error on first signup (check logs)
3. Resend API rate limit
4. Contacts already in audience (silent duplicate)

**Fix:**
```bash
# Check config
uv run python -c "
from padelnomics.core import config
print('API key:', 'Set' if config.RESEND_API_KEY else 'Not set')
"

# Check resend_audiences table for cached IDs
uv run python -c "
import sqlite3
conn = sqlite3.connect('data/app.db')
rows = conn.execute('SELECT * FROM resend_audiences').fetchall()
for row in rows:
    print(row)
"
```

### Decorator not intercepting GET requests

**Symptoms:** Waitlist mode enabled, but route shows normal page

**Causes:**
1. Decorator applied in wrong order
2. Missing import
3. Decorator syntax error

**Fix:**
```python
# Correct order:
@bp.route("/route", methods=["GET", "POST"])
@csrf_protect
@waitlist_gate("template.html")
async def route():
    ...

# NOT:
@waitlist_gate("template.html")
@csrf_protect  # <- csrf must come after route decorator
@bp.route("/route", methods=["GET", "POST"])
```

---

## Migration Guide

If you already have waitlist logic copy-pasted across routes, here's how to migrate:

### Before (duplicated code)

```python
@bp.route("/signup", methods=["GET", "POST"])
@csrf_protect
async def signup():
    if config.WAITLIST_MODE:
        if request.method == "GET":
            return await render_template("waitlist.html")

        # POST - capture email
        form = await request.form
        email = form.get("email", "").strip().lower()

        if not email or "@" not in email:
            await flash("Invalid email", "error")
            return redirect(url_for("auth.signup"))

        # Insert into DB
        await execute(
            "INSERT OR IGNORE INTO waitlist (email, intent, ip_address) VALUES (?, ?, ?)",
            (email, "signup", request.remote_addr)
        )

        # Enqueue email
        await enqueue("send_waitlist_confirmation", {"email": email, "intent": "signup"})

        # Add to Resend (old: manual audience ID required)
        if config.RESEND_AUDIENCE_WAITLIST:
            try:
                resend.Contacts.create(...)
            except:
                pass

        return await render_template("waitlist_confirmed.html", email=email)

    # Normal flow...
```

### After (using decorator + helper)

```python
from ..core import capture_waitlist_email, waitlist_gate

@bp.route("/signup", methods=["GET", "POST"])
@csrf_protect
@waitlist_gate("waitlist.html")
async def signup():
    if config.WAITLIST_MODE and request.method == "POST":
        form = await request.form
        email = form.get("email", "").strip().lower()

        if not email or "@" not in email:
            await flash("Invalid email", "error")
            return redirect(url_for("auth.signup"))

        await capture_waitlist_email(email, intent="signup")
        return await render_template("waitlist_confirmed.html", email=email)

    # Normal flow...
```

**Lines saved:** 40+ → 10 (75% reduction)

---

## FAQ

### Q: Can I use waitlist mode in production?

**A:** Yes! That's the point. Enable it when you want to validate demand before building features.

### Q: What happens to existing users when I enable waitlist mode?

**A:** Logged-in users can still access the app normally. Waitlist gates only apply to unauthenticated routes (signup, supplier signup, export). Consider adding a "coming soon" banner for logged-in users if gating features they can see.

### Q: Can I customize the waitlist email template?

**A:** Yes. Edit `handle_send_waitlist_confirmation()` in `src/padelnomics/worker.py`. You can change subject, body, sender, and add HTML formatting.

### Q: Can I add custom fields to the waitlist form?

**A:** Yes, but keep it minimal (email + 1-2 fields max). Add columns to the `waitlist` table via a migration, then update `capture_waitlist_email()` and your route to capture them.

### Q: How do I prevent spam signups?

**A:** Current measures:
- CSRF token required
- Email validation (must contain `@`)
- IP address captured (for manual review)
- Rate limiting (inherited from app-wide rate limits)

Additional measures:
- Add honeypot field to form
- Add Cloudflare Turnstile captcha
- Add email verification (double opt-in)

### Q: Can I A/B test waitlist messaging?

**A:** Yes! Use the existing `@ab_test` decorator:

```python
@bp.route("/signup", methods=["GET", "POST"])
@csrf_protect
@ab_test("waitlist_messaging", variants=("short", "long"))
@waitlist_gate("waitlist.html", variant=lambda: g.ab_variant)
async def signup():
    ...
```

Then create two template versions based on `variant`.

### Q: How do I export the waitlist?

**A:**
```bash
uv run python -c "
import sqlite3, csv
conn = sqlite3.connect('data/app.db')
rows = conn.execute('SELECT email, intent, created_at FROM waitlist ORDER BY created_at').fetchall()

with open('waitlist_export.csv', 'w', newline='') as f:
    writer = csv.writer(f)
    writer.writerow(['email', 'intent', 'created_at'])
    writer.writerows(rows)

print('Exported', len(rows), 'emails to waitlist_export.csv')
"
```

### Q: Can I use waitlist mode for some routes but not others?

**A:** Yes. `WAITLIST_MODE` is a global flag, but you can add conditional logic:

```python
# Only gate this route if WAITLIST_MODE is enabled
@waitlist_gate("waitlist.html") if config.WAITLIST_MODE else lambda f: f
async def route():
    ...
```

Or create a feature-specific flag:

```python
# core.py
class Config:
    WAITLIST_MODE_EXPORT: bool = os.getenv("WAITLIST_MODE_EXPORT", "false").lower() == "true"

# routes.py
@waitlist_gate("export_waitlist.html") if config.WAITLIST_MODE_EXPORT else lambda f: f
async def export():
    ...
```

---

## Summary

Waitlist mode is a lean, no-frills feature validation tool. Enable it with a single env var, capture emails with two abstractions, and disable it when you're ready to launch. No dependencies, no SaaS subscriptions, no complexity.

**Key files:**
- `src/padelnomics/core.py` — `waitlist_gate()` decorator + `capture_waitlist_email()` helper
- `src/padelnomics/auth/routes.py` — Example: entrepreneur signup
- `src/padelnomics/suppliers/routes.py` — Example: supplier signup with plan-specific messaging
- `src/padelnomics/planner/routes.py` — Example: feature gate (export)
- `src/padelnomics/worker.py` — Email confirmation task handler
- `tests/test_waitlist.py` — Comprehensive test coverage

**Architecture:**
- SQLite table with email, intent, plan, ip_address
- Decorator intercepts GET requests, routes handle POST
- Helper handles DB + email + Resend (all silent failures)
- Worker sends confirmations via `send_waitlist_confirmation` task

**Usage:**
1. Set `WAITLIST_MODE=true`
2. Add `@waitlist_gate("template.html")` to routes
3. Call `await capture_waitlist_email(email, intent)` in POST handler
4. Create waitlist + confirmation templates
5. Test, promote, monitor signups
6. Export emails, launch, disable waitlist mode
