---
id: w4/m7/t001
title: "SendGrid SMTP relay + secret plumbing (.env → auth-secrets.sh → gh-secrets), recorded in docs/ADR012-auth.md"
worker: worker4
status: done
estimate: 25m
depends_on: []
---

## Objective

Use **SendGrid** as the prod mail provider (user decision 2026-07-06), integrated purely as an SMTP relay — no vendor SDK: Kratos's courier takes `smtps://apikey:<sendgrid-api-key>@smtp.sendgrid.net:465` (literal username `apikey`), so the mechanism stays a provider-agnostic connection URI that self-hosters can point anywhere. Plumb the secret the same way as the Ory secrets: a `KRATOS_COURIER_SMTP_URI` key in `.env` (names mirrored to `.env.example`/`.env.template`, never values), injected into the `auth/kratos` Secret by `scripts/auth-secrets.sh`, pushed to GitHub Actions by `scripts/gh-secrets.sh`. _2026-07-11: the same relay feeds a second consumer — bex-api's already-shipped workspace-invite mailer (w4/m12) reads `BEX_SMTP_ADDR/FROM/USERNAME/PASSWORD`; plumb those names through the same `.env` → templates → gh-secrets path in this pass so one relay closes both consumers._

## Context

- Pattern: docs/ADR012-auth.md §5 — secrets out-of-band, composed/validated by `scripts/auth-secrets.sh`, `DRY_RUN=1` prints names only.
- Chart mechanics (0.62.1, `secret.enabled: false`): the chart injects `COURIER_SMTP_CONNECTION_URI` from the out-of-band `kratos` Secret under the **exact key `smtpConnectionURI`** and omits `connection_uri` from the rendered configmap — a URI written into values is only a non-empty gate, never the delivered value. Base values carry a non-secret placeholder (e.g. `set-via-secret`) in `courier.smtp.connection_uri` to gate the env injection (t003).
- SendGrid deliverability prerequisites (account-side, document rather than automate): sender/domain authentication for the from-address domain (SPF + DKIM DNS records), and the from-address itself in the Kratos courier values.
- Locally the Mailpit URI also arrives through the Secret, not overlay values: run `auth-secrets.sh` with `KRATOS_COURIER_SMTP_URI=smtp://mailpit.<ns>.svc:1025` (t002/t003).
- _2026-07-11:_ w4/m12's invite mailer (`lego/backend/cmd/api/main.go`, `internal/mailer/`) is silently nil in prod — no `BEX_SMTP_*` env on the bex-api deployment, and the var names are missing from `.env.example`/`.env.template` (violating the mirror rule) even though `CLAUDE.md`'s env table already promises "same relay as the Kratos courier". This task fixes the templates/secrets half; t005 wires the deployment.

## Steps

1. Record the decision (SendGrid via SMTP relay URI; mechanism stays provider-agnostic for self-hosters) plus the SPF/DKIM prerequisites as a short addition to `docs/ADR012-auth.md`.
2. Add `KRATOS_COURIER_SMTP_URI` handling to `scripts/auth-secrets.sh` (presence/shape validation, never echo) — written into the `kratos` Secret under the chart key `smtpConnectionURI`.
3. Mirror `KRATOS_COURIER_SMTP_URI` **and** the `BEX_SMTP_ADDR/FROM/USERNAME/PASSWORD` quartet (names + comments, never values) into `.env.example` and `.env.template`; add the secret-bearing keys to `scripts/gh-secrets.sh`.
4. `npx prettier@3.4.2 --write "**/*.md"`.

## Files

- `scripts/auth-secrets.sh`, `scripts/gh-secrets.sh`
- `.env.example`, `.env.template`
- `docs/ADR012-auth.md`

## Acceptance criteria

- [ ] `auth-secrets.sh` writes the SMTP URI into the `auth/kratos` Secret **under the chart key `smtpConnectionURI`** when the env var is set, and fails with a clear message when required and absent.
- [ ] Var names (courier URI + `BEX_SMTP_*` quartet) mirrored in both env templates; no value ever printed.
- [ ] Docs name SendGrid as the default prod provider and list the SPF/DKIM prerequisites, while stating the URI is provider-agnostic.

## Out of scope

- Enabling the courier (t003); wiring `BEX_SMTP_*` into the bex-api deployment (t005); SendGrid account creation and DNS record changes (operator-side, out-of-band).
