# SHEP Bar Prep — Email Journey Flows

Last updated: 2026-05-01 (implementation complete, PR #179 in review)

---

## Overview

Five automated emails cover four distinct student journeys. Every email is event-triggered (no time-based drip schedule) and fires at most once per student per trigger.

All emails use the Brevo transactional SMTP API. Reply-to is `help@shepbarprep.com`.

---

## The Five Emails

### 1. Welcome
- **Trigger:** Immediately on registration
- **Paths:** A, B, D
- **Subject line:** Welcome to SHEP Bar Prep
- **Content:** 3-step visual breakdown (Write under timed conditions → Get scored on the rubric → See exactly what you missed), CTA to start free demo essay
- **CTA:** Start Your Free Essay → `/demo/mee`
- **Guard:** Send once per user (on registration event)

### 2. Score Report
- **Trigger:** Immediately when demo essay is graded and student has a real email
- **Paths:** B, C
- **Subject line:** Your SHEP Score Report: {Subject}
- **Content:** Band X/6 hero, component score bars (colored per dimension), two-column spotted/missed issues, top improvement priorities, CTA to purchase
- **CTA:** Get Full Access → `/purchase/mee`
- **Guard:** `score_report_sent_at IS NULL` atomic UPDATE (fires once per lead)
- **Status:** Implemented in `src/lib/server/score-report-email.ts`

### 3. Demo Nudge
- **Trigger:** 48 hours after registration, student has not started a demo
- **Paths:** A, D
- **Subject line:** Your free essay is still waiting
- **Content:** Short personal nudge, mentions the demo is Torts (the default demo subject), reassurance that the free attempt doesn't expire
- **CTA:** Start Your Free Essay → `/demo/mee`
- **Guard:** Only send if user has no `diagnostic_leads` record with source `free-diagnostic`

### 4. Register Nudge
- **Trigger:** 48 hours after completing a demo without registering (Path C only)
- **Paths:** C
- **Subject line:** Your score report is saved
- **Content:** Highlights benefits of registering (track progress, try more subjects, get full access), checklist format
- **CTA:** Create Your Account → `/login`
- **Guard:** Only send to `diagnostic_leads` with a real email, no matching `users` record, and `register_nudge_sent_at IS NULL`

### 5. Abandoned Cart
- **Trigger:** 24 hours after viewing `/checkout` without completing purchase
- **Paths:** B, D
- **Subject line:** Your free essay is waiting
- **Content:** Value prop reinforcement, SHEP15 promo code ($15 off), CTA to free demo
- **CTA:** Start Your Free Essay → `/demo/mee`
- **Guard:** `abandoned_cart_email_sent_at IS NULL`, `subscription_status = 'TRIAL'`, `checkout_viewed_at` > 24h ago
- **Status:** Implemented in `src/routes/api/cron/abandoned-cart/+server.ts`, runs hourly via Vercel cron

---

## The Four Journey Paths

### Path A: Landing → Register → disappears

Student signs up but never tries the demo or visits checkout.

```
Register
  ↓ (immediate)
[1. Welcome]
  ↓ (48h, no demo started)
[3. Demo Nudge]
  ↓
  (end — no further engagement signal)
```

**Current gap:** No emails exist for this path. Welcome and Demo Nudge are both new.

### Path B: Landing → Demo → Score Report → Register → Checkout → leaves

Full funnel traversal without converting.

```
Demo completed + email provided
  ↓ (immediate)
[2. Score Report]

Register
  ↓ (immediate)
[1. Welcome]

View /checkout
  ↓ (24h, no purchase)
[5. Abandoned Cart]
```

**Current coverage:** Score Report and Abandoned Cart exist. Welcome is new.

### Path C: Direct link → Demo → Score Report → never registers

Student arrives from a referral, ad, or Reddit post. Completes the demo but doesn't create an account.

```
Demo completed + email provided
  ↓ (immediate)
[2. Score Report]
  ↓ (48h, no registration)
[4. Register Nudge]
```

**Current coverage:** Score Report exists. Register Nudge is new.

### Path D: Register → Checkout → leaves (skips demo)

Student knows what they want, goes straight to checkout, gets cold feet.

```
Register
  ↓ (immediate)
[1. Welcome]

View /checkout
  ↓ (24h, no purchase)
[5. Abandoned Cart]

  ↓ (48h after registration, no demo started)
[3. Demo Nudge]
```

**Current coverage:** Abandoned Cart exists. Welcome and Demo Nudge are new.

---

## Implementation Status

All five emails are implemented. PR #178 (score report redesign) merged 2026-05-01. PR #179 (journey emails) in review.

| Email | Status | Template | Trigger |
|-------|--------|----------|---------|
| Welcome | Done | `src/lib/server/welcome-email.ts` | `src/routes/api/auth/register/+server.ts` (fire-and-forget) |
| Score Report | Done | `src/lib/server/score-report-email.ts` | `src/routes/demo/mee/results/+page.server.ts` (on grade) |
| Demo Nudge | Done | `src/lib/server/demo-nudge-email.ts` | `src/routes/api/cron/demo-nudge/+server.ts` (hourly) |
| Register Nudge | Done | `src/lib/server/register-nudge-email.ts` | `src/routes/api/cron/register-nudge/+server.ts` (hourly) |
| Abandoned Cart | Done | inline in `+server.ts` | `src/routes/api/cron/abandoned-cart/+server.ts` (hourly) |

### Shared infrastructure

- **`src/lib/server/email-templates.ts`** — shared wrapper with logo header ("SHEP Bar Prep" inside card), CTA button, reply line, footer with CAN-SPAM address, unsubscribe link (opt-in per email). Exports design token constants (`EMAIL_BRAND_PRIMARY`, etc.).
- **`src/lib/server/email.ts`** — Brevo transactional SMTP API wrapper (all emails go through this).
- **`static/logo-email.png`** — 9.8KB optimized logo for email (128px wide, retina-ready).

### Database columns

- `users.demo_nudge_sent_at` — tracks demo nudge delivery (migration 031)
- `users.abandoned_cart_email_sent_at` — tracks abandoned cart delivery (migration 029)
- `users.checkout_viewed_at` — tracks checkout page visit (migration 029)
- `diagnostic_leads.register_nudge_sent_at` — tracks register nudge delivery (migration 031)
- `diagnostic_leads.score_report_sent_at` — tracks score report delivery (migration 030)
- `idx_users_email` — index for cross-table NOT EXISTS queries (migration 031)

### Cron configuration (vercel.json)

All three crons run hourly (`0 * * * *`), authenticated via `CRON_SECRET` bearer token:
- `/api/cron/abandoned-cart`
- `/api/cron/demo-nudge`
- `/api/cron/register-nudge`

### Query guard details

**Demo Nudge eligibility:**
- `created_at` > 48h ago
- `subscription_status = 'TRIAL'`
- `demo_nudge_sent_at IS NULL`
- `abandoned_cart_email_sent_at IS NULL` (prevents overlap)
- `is_test_user = false`
- No `diagnostic_leads` record with matching email + `source = 'free-diagnostic'`
- `LIMIT 10`, `FOR UPDATE SKIP LOCKED`

**Register Nudge eligibility:**
- `source = 'free-diagnostic'`
- `grade_result_json IS NOT NULL`
- `graded_at` > 48h ago (uses grading timestamp, not creation)
- `email NOT LIKE '%@demo.local'`
- `register_nudge_sent_at IS NULL`
- `linked_user_id IS NULL`
- No `users` record with matching email
- `isInternalTestEmail` check in code
- `LIMIT 10`, `FOR UPDATE SKIP LOCKED`

### Known limitations / follow-ups

- [ ] **BAR-126:** Host pentagon radar chart as server-rendered PNG for score report email
- [ ] Migrate abandoned-cart and score-report email templates to use shared `email-templates.ts` wrapper
- [ ] Add `List-Unsubscribe` email header to Brevo API payload (currently footer link only)
- [ ] Build `/unsubscribe` route (nudge email footer links to it but it doesn't exist yet)
- [ ] TDD for cron query logic (demo-nudge and register-nudge SQL filters)
- [ ] Consider `waitUntil` for welcome email to survive Vercel function lifecycle

---

## Design Notes

- **Tone:** Personal, not salesy. Each email reads like it came from a small team, not a marketing funnel.
- **Frequency:** A student receives at most 1 email per event. No time-based drip sequences.
- **Promo code:** Only the Abandoned Cart email includes SHEP15 ($15 off). Other emails focus on value, not discounts.
- **Demo subject:** The default free demo essay is Torts (`torts_003`). Do not reference "Criminal Law" in emails about the demo.
- **Logo:** Emails use `logo-email.png` (9.8KB optimized). Sized at `width:44px; height:auto`.
- **Reply-to:** All emails reply to `help@shepbarprep.com` (not founder@).
- **Pentagon chart:** Inline SVG doesn't work in email (stripped by Gmail, Fastmail, Outlook). BAR-126 tracks hosting the pentagon as a server-rendered PNG. Until then, component scores use colored horizontal bars.
