nook: capture trial email and run the two lifecycle emails

Trials were anonymous — a 14-day trial you cannot talk to. trials gains
an email column (idempotent ALTER), POST /api/the-nook/trial-email
attaches the optional first-launch prompt's answer, and the address
lands in a Brevo list when NOOK_TRIAL_LIST_ID is set. The lifecycle
emails follow: a D+11 trial expiry reminder with a buy link, a purchase
thank-you from the Stripe webhook, and scheduled testimonial (D+3) and
review (D+14) asks via the daily /api/the-nook/lifecycle cron. Rows are
marked before sending so retries never double-send; trial addresses
that already bought get no buy reminder; the endpoint requires a
Bearer secret (CRON_SECRET / NOOK_LIFECYCLE_SECRET) and fails closed
when unconfigured — an open trigger would email real users. All sends
are best-effort and logged, never thrown.

Tests: trial-email (3) and lifecycle (6) suites; full run 539 pass.

Issues: FRE-10, FRE-11
This commit is contained in:
2026-10-04 23:22:24 -04:00
parent 421fb946fc
commit f66fd43f62
10 changed files with 661 additions and 3 deletions

View File

@@ -83,3 +83,13 @@ NOOK_LICENSE_PRIVATE_KEY="<generate-license-keypair>" # base64 PKCS8
NOOK_STRIPE_SK="sk_live_<rotate-in-stripe-dashboard>"
NOOK_STRIPE_WEBHOOK_SECRET="whsec_<rotate-in-stripe-dashboard>"
NOOK_STRIPE_PRICE_ID="price_<from-stripe-dashboard>"
# ── The Nook lifecycle emails ──
# Daily cron (vercel.json → /api/the-nook/lifecycle) requires a Bearer secret.
# Set CRON_SECRET and Vercel attaches it automatically to scheduled runs.
# Generate: openssl rand -base64 64 | tr -d '\n'
CRON_SECRET="<openssl-rand-base64>"
# Optional override for the secret the endpoint accepts (falls back to CRON_SECRET).
NOOK_LIFECYCLE_SECRET=""
# Optional Brevo list id trial-start emails are added to (the lifecycle audience).
NOOK_TRIAL_LIST_ID=""