Switch newsletter signup to Brevo double opt-in

Was a plain POST /v3/contacts upsert (single opt-in — straight onto the
list, no confirmation required). Now calls doubleOptinConfirmation
instead, so a signup only requests subscription; Brevo sends its own
confirmation email and adds the contact to the real list only once they
click through. Needs BREVO_DOUBLE_OPTIN_TEMPLATE_ID set in Coolify
before this works — not yet configured, signups will fail closed with a
logged reason until it is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-25 13:40:27 +00:00
parent bab2c916be
commit df4bd700e6
2 changed files with 58 additions and 24 deletions
+22 -8
View File
@@ -53,8 +53,12 @@ on the Payload side — this app's SMTP connection is deliberately
independent, see the "Monitoring & alerting" section). `STRIPE_SECRET_KEY`,
`STRIPE_WEBHOOK_SECRET`, `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`,
`PAYMENT_WEBHOOK_SECRET`, `PAYMENT_TEST_MODE` — see "Payment processing
(Stripe)" below. Set in Coolify's app settings for production, not in a
committed `.env`.
(Stripe)" below. `BREVO_API_KEY`, `BREVO_LIST_ID`,
`BREVO_DOUBLE_OPTIN_TEMPLATE_ID` (no safe default — required for
newsletter signups to trigger a confirmation email at all),
`BREVO_DOI_REDIRECT_URL` (optional, defaults to the homepage) — see
"Newsletter signup" below. Set in Coolify's app settings for production,
not in a committed `.env`.
## Pages
@@ -1057,15 +1061,22 @@ unsynced.
- **`app/lib/brevo.ts`** — the only thing that talks to Brevo.
`upsertNewsletterContact(email, source)` calls Brevo's
`POST /v3/contacts` with `updateEnabled: true` (204 for both a new and
an existing contact — no special-casing needed) and a
`BREVO_LIST_ID`-scoped list membership. `source` (`"checkout"` |
**double opt-in** endpoint, `POST /contacts/doubleOptinConfirmation`
(switched 2026-07-25 from the plain `POST /v3/contacts` single-opt-in
upsert this originally shipped with) — this only ever *requests* a
subscription; Brevo sends a confirmation email (the template at
`BREVO_DOUBLE_OPTIN_TEMPLATE_ID`, configured as the list's Double Opt-in
template in Brevo's own UI) and only actually adds the contact to
`BREVO_LIST_ID` once they click through. `source` (`"checkout"` |
`"newsletter-page"` | `"newsletter-modal"` | `"newsletter-hero"` |
`"challenge"`) is stored as the contact's `OPT_IN_SOURCE` attribute for
segmentation — that attribute has to already exist on the Brevo account
(`POST /v3/contacts/attributes/normal/OPT_IN_SOURCE`) or Brevo silently
drops it on every upsert (no error at all, just never stored) rather
than rejecting the request.
drops it on every request (no error at all, just never stored) rather
than rejecting the request. `redirectionUrl` (where Brevo sends the
contact after they click confirm) defaults to the homepage via
`BREVO_DOI_REDIRECT_URL` — no dedicated "danke, bestätigt" landing page
exists yet.
- **`app/lib/useNewsletterSignup.ts`** — the shared email/consent/submit
state + on-blur validation + refocus-on-invalid-submit behind all four
forms (same "state of the art, simple" input-quality bar as checkout's
@@ -1091,7 +1102,10 @@ unsynced.
at all, so that piece can only be built/inspected in Brevo's own UI, not
from this codebase.
- Needs `BREVO_API_KEY`/`BREVO_LIST_ID` set in the deployment environment
— confirmed live end-to-end 2026-07-23.
— confirmed live end-to-end 2026-07-23. As of the double-opt-in switch,
also needs `BREVO_DOUBLE_OPTIN_TEMPLATE_ID` (no safe default — every
signup silently no-ops without it) and optionally
`BREVO_DOI_REDIRECT_URL`.
## Orders & customer accounts