How it works
SendGrid keepstools.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
- Ensure
apps/backend/package.jsoncontainstooling-mailer-twilio-sendgridas a workspace dependency. - Run
make deps-install. - Set SendGrid config in
apps/backend/src/configuration/production.tsorapps/backend/src/configuration/staging.ts:
- Add environment variables:
- Add/update tests in
apps/tools/mailer/twilio-sendgrid/__tests__/buildMailer.spec.tsandapps/backend/src/tools/mailer/__tests__/loader.spec.ts. - Run:
What not to do
❌ Never call provider APIs directly from resolvers. ❌ Never skip the overlap validation forjoinandleave. ❌ Never log unmasked user emails/phones in mailer logs.
Live API smoke (optional)
This is for local development only — do not wiresmoke: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.
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:
SENDGRID_SMOKE_* in .env.development):
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 inapps/tools/mailer/twilio-sendgrid/src/ with a single package entrypoint in src/index.ts. Keep package tests in apps/tools/mailer/twilio-sendgrid/__tests__/.
