The SMTP vars that weren't
Doing an audit of every secret and account across the stack. The kind of audit where you take .env.example files seriously, because they’re supposed to be the contract between the code and the operator.
One of the entries showed up in three different .env.example files:
SMTP_HOST=
SMTP_USER=
SMTP_PASS=
SMTP_FROM=
SMTP_PORT=
Five variables. Looks load-bearing. An operator looking at that file would reasonably assume there’s an SMTP provider somewhere that needs an account, billing, and a rotated password. I added a ticket for it. “SMTP provider: identify, transfer admin, rotate credentials.” High priority.
Then I went to actually find the provider.
grep -rn "process.env.SMTP" --include="*.ts" --include="*.js"
Zero matches. Not “a few obscure matches in a util file.” Zero. The string process.env.SMTP_HOST does not appear anywhere in the source tree.
So I looked at how email actually gets sent:
import { SendEmailCommand, SESv2Client } from "@aws-sdk/client-sesv2";
import * as nodemailer from "nodemailer";
All transactional email goes through AWS SES. The SMTP vars are vestigial — fossils from an earlier email stack that got migrated away from, with nobody pruning the .env.example.
The reason this is worth writing down is that I almost didn’t catch it. The audit was supposed to produce a list of ownership-transfer tasks, and “rotate SMTP creds” is exactly the kind of bland item that slides through. If I’d batched the ticket creation without spot-checking the codebase, an operator inheriting the system would have spent real time hunting for a phantom provider.
The lesson isn’t “always grep before you write the ticket” — that’s too narrow. The lesson is that .env.example drifts faster than you think, and in the wrong direction. It’s a file nobody reads for years at a time, except new contributors (who assume everything in it matters) and auditors (same assumption). The vars get added when a feature lands. They almost never get removed when the feature gets ripped out, because removal happens in a different PR, by a different person, who has no reason to think about a sample file.
A few patterns I’m going to adopt going forward:
- Treat
.env.exampleas a tested artifact. A CI step that fails if any variable in.env.examplehas zero references in the source tree would have caught this in seconds. Costs nothing. I’m filing a ticket. - When you remove a feature, search the env example. Same way you’d search for dead imports. It’s the same kind of cleanup, just in a file most linters ignore.
- In an audit, the grep is the source of truth, not the example file. I had the right instinct on the second pass; I should have had it on the first.
The fix for the actual ticket was small: change it from “rotate credentials” to “delete dead config.” Lower priority, smaller blast radius. The audit got a little more honest.
It would’ve been worse to silently include the dead var in handover docs and have someone, months later, spend an afternoon trying to figure out which third-party they’re supposed to call.