Skip to main content
This page covers how the Twilio SendGrid mailer package is wired, configured, and extended for transactional email plus CRM create, update, and destroy operations.

How it works

SendGrid keeps tools.mailer.from as { email, name } (twilioSendgridSchema in apps/shared/src/mailer/schema.ts) — there is no numeric sender id in config. Brevo uses the same from shape; it resolves Brevo’s transactional sender id at send time; see Brevo mailer. The backend chooses a mailer implementation from config, then delegates to the package contract. SendGrid handles both transactional delivery and CRM contact operations through one adapter boundary. The CRM API follows shared input contracts:
join and leave overlap is a hard validation error and returns a standardized failure result.

Extending this system

  1. Ensure apps/backend/package.json contains tooling-mailer-twilio-sendgrid as a workspace dependency.
  2. Run make deps-install.
  3. Set SendGrid config in apps/backend/src/configuration/production.ts or apps/backend/src/configuration/staging.ts:
  1. Add environment variables:
  1. Add/update tests in apps/tools/mailer/twilio-sendgrid/__tests__/buildMailer.spec.ts and apps/backend/src/tools/mailer/__tests__/loader.spec.ts.
  2. Run:

What not to do

❌ Never call provider APIs directly from resolvers. ❌ Never skip the overlap validation for join and leave. ❌ Never log unmasked user emails/phones in mailer logs.

Live API smoke (optional)

This is for local development only — do not wire smoke:api into CI (it requires a real API key and can send email or create/delete contacts). Manually verify SendGrid v3 (auth, optional transactional send, and Marketing contact lifecycle) using the same request shapes as apps/tools/mailer/twilio-sendgrid/src/index.ts. Run the tooling-mailer-twilio-sendgrid package script inside the backend container. Never commit API keys. When Twilio SendGrid is your mailer in local dev, SENDGRID_API_KEY is already loaded into the backend service from .env.development (same env_file as Compose uses for the app). You do not need docker compose exec -e SENDGRID_API_KEY=... unless you are overriding the key for a one-off run. Marketing contact create is asynchronous; the script waits SENDGRID_SMOKE_ASYNC_WAIT_SEC seconds (default 4) before searching for the new contact. Increase it if step 6 flakes.
403 on /marketing/*: the key is valid for auth (e.g. GET /user/profile succeeds) but lacks Marketing API permissions. Run smoke:api: it calls GET /v3/scopes and warns if marketing_campaigns.* is missing (that list is authoritative for this key). Fix by enabling Marketing Campaigns on the account and/or using a key that includes those scopes — see SendGrid authorization and retrieve scopes.
Without SENDGRID_SMOKE_*, GET /user/profile and GET /scopes still run (validates the key and prints which scopes it has, including whether marketing_campaigns.* is present). For the full suite, either add SENDGRID_SMOKE_* to .env.development and recreate/restart backend if needed, or pass them only on exec:
Typical one-liner (key already in container; optional SENDGRID_SMOKE_* in .env.development):
The script runs: GET /user/profile, GET /scopes (with a WARN if marketing_campaigns.* is absent while Marketing env vars are set), optional POST /mail/send, then (when list + email are set) preclean delete (if found), create (PUT /marketing/contacts), wait, search, update (PUT /marketing/contacts), list add (PUT /marketing/lists/{id}/contacts), delete (DELETE /marketing/contacts) — and exits non-zero if any expected status is wrong.

File conventions

New implementation code goes in apps/tools/mailer/twilio-sendgrid/src/ with a single package entrypoint in src/index.ts. Keep package tests in apps/tools/mailer/twilio-sendgrid/__tests__/.

What’s next?