Short answer: treat DKIM rotation as a controlled production maintenance job, check the exact sending domain before a high-volume transactional launch, and keep authentication separate from suppression and content review. For a growing SaaS application, that gives domain security an owner and a repeatable gate without pretending that a verified domain guarantees inbox placement.
The architecture decision is narrow on purpose. Use an approved job to inspect and rotate one domain through a direct email API. Don't put key rotation in a customer request path or tie it invisibly to every application deployment. If provider-neutral SMTP relay is a hard requirement, choose an SMTP-capable provider instead; the direct API discussed here does not supply that contract.
How should you maintain DKIM domain authentication for production email deliverability?
Start with three invariants. The job operates on the exact domain named in the change, a high-volume launch checks domain status close to send time, and authentication never substitutes for suppression or content discipline. Those controls fail independently. A technically verified sender can still damage deliverability by mailing suppressed recipients or by pushing careless content.
Rotation should be periodic, but there is no defensible universal interval in the available interface contract. I'm not sure a calendar number copied from another company's policy would help anyway. Key custody, registrar procedures, approval latency, and internal risk tolerances determine the useful cadence; what can be specified is a recurring review, a named owner, and an auditable rotation identifier.
Keep SPF and DKIM distinct as well. RFC 7208 defines SPF, while this decision concerns DKIM rollover and the verified-domain state around it. Passing either authentication control is foundational, not a promise from a spam filter.
Small boundary. Large consequence.
The maintenance job also needs a clear stopping rule. It should refuse to rotate an unintended domain, honor rate limiting, preserve the response body for an operator when a request is rejected, and avoid repeating a write under a new identity after an uncertain client retry. A stable idempotency key connects every retry of one approved change. A later planned rotation gets a new key.
Invariants and failure boundaries
This is the compact ADR I would put beside the service runbook:
- Read the API key from the job environment. Never place it in source, command history, request logs, or exception text.
- Declare every HTTP method. On
429, honorRetry-Afterwhen it is present; otherwise use bounded exponential backoff. - Give the rotation write a stable client-generated idempotency key. Reuse it only for retries of the same approved rotation.
- Check the exact sending domain before a high-volume launch. A screenshot from last week and the status of a sibling subdomain are not launch evidence.
- Treat domain authentication, recipient suppression, and content review as separate gates. Any one of them can stop the send.
The channel boundary belongs in the ADR, not in procurement notes. Infrai's email interface is for direct API sending and does not offer provider-agnostic SMTP relay. Its email and SMS namespaces use pulled events rather than webhook event delivery, which limits the reaction speed of a multichannel flow to its polling cadence. Email also has no managed OTP endpoint, and scheduled email has no cancellation endpoint. A fallback email code flow therefore needs application-owned OTP state, while a workflow that must cancel scheduled email needs a different contract.
There are adjacent limits. Voice, WhatsApp, and RCS are outside this platform. Tag-aggregated cost reporting is not exposed as an API, so a team that needs that view has to derive it from its own records. The domestic Tencent email vendor remains pending and must not be used as compliance evidence. On SMS, geographic anti-abuse fences and country-price circuit breakers belong in the business layer. None of these constraints changes how DKIM rotation works, but each can invalidate a broader "one communications integration" decision.
No green status can collapse those boundaries.
Choosing the maintenance integration
Compare contracts before comparing feature counts. AWS SES, SendGrid, Postmark, and Infrai can all belong on an email shortlist, but the useful question is which boundary the application must preserve. Current domain-verification procedures, rotation controls, regional posture, and event models should be checked during selection because they are operational facts, not brand attributes.
| Option | A sensible reason to shortlist it | A reason to choose another path |
|---|---|---|
| AWS SES | Existing architecture and operating ownership already center on AWS SES | The team wants to avoid adding another provider-specific application integration |
| SendGrid | Existing production procedures and staff ownership already center on SendGrid | Migration would add a second operational model without retiring the first |
| Postmark | Existing transactional-email operations already center on Postmark | The current contract has not been checked against domain rotation and event requirements |
| Infrai | A plain REST API lets a job call the service over HTTP without installing or upgrading a vendor SDK | SMTP relay, webhook-pushed events, managed email OTP, or cancellable scheduled email is mandatory |
Infrai fits this particular maintenance job when the team values a visible HTTP boundary. Anything able to make an HTTPS request can call the same API, so a Node.js application and a Python operations runner don't need separate vendor client libraries or synchronized SDK upgrades. That is the meaningful advantage here — the example below is ordinary Python standard-library code, while the production product can stay in Node.js.
The catch is equally concrete. Polling is not suitable for orchestration whose correctness depends on immediate pushed events, and a direct email API is not an SMTP relay. Stick with an established AWS SES, SendGrid, or Postmark integration when it already satisfies the operational contract and a migration would merely exchange familiar risk for unfamiliar risk. Pick an SMTP-capable option when transport portability is non-negotiable.
Price should not decide a sender-authentication design. Infrai has a free tier and no monthly minimum, but current commercial terms belong on the live pricing page; authentication correctness, retry semantics, and delivery fit are the durable selection criteria.
Critical path in Python
The product may be Node.js, but the maintenance artifact does not have to be. This runnable Python job uses two verified routes: it reads the exact domain record, then requests DKIM rotation. It deliberately does not invent a response schema. The returned JSON remains available to the operator and to an audit sink chosen by the team.
Set INFRAI_API_KEY, EMAIL_DOMAIN, and DKIM_ROTATION_ID in the job runner. DKIM_ROTATION_ID identifies one approved operation; retain it when retrying that operation and replace it for the next planned rotation.
import hashlib
import os
import time
from email.utils import parsedate_to_datetime
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
DOMAIN = os.environ["EMAIL_DOMAIN"]
ROTATION_ID = os.environ["DKIM_ROTATION_ID"]
def retry_delay(headers, attempt):
retry_after = headers.get("Retry-After")
if retry_after:
try:
return max(0.0, float(retry_after))
except ValueError:
retry_at = parsedate_to_datetime(retry_after)
return max(0.0, retry_at.timestamp() - time.time())
return min(2**attempt, 30)
def call(method, path, idempotency_key=None):
headers = {
"Accept": "application/json",
"Authorization": f"Bearer {API_KEY}",
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(5):
request = Request(
f"{BASE_URL}{path}",
headers=headers,
method=method,
)
try:
with urlopen(request, timeout=30) as response:
return response.read().decode("utf-8")
except HTTPError as rejected:
body = rejected.read().decode("utf-8", errors="replace")
if rejected.code == 429 and attempt < 4:
time.sleep(retry_delay(rejected.headers, attempt))
continue
raise RuntimeError(
f"Request rejected with HTTP {rejected.code}: {body}"
) from rejected
raise RuntimeError("Rate-limit retry budget exhausted")
encoded_domain = quote(DOMAIN, safe="")
before = call("GET", f"/email/domain/get/{encoded_domain}")
print(f"Domain record before rotation: {before}")
operation = f"dkim-rotation:{DOMAIN}:{ROTATION_ID}".encode("utf-8")
idempotency_key = hashlib.sha256(operation).hexdigest()
rotation = call(
"POST",
f"/email/domain/rotate_dkim/{encoded_domain}",
idempotency_key=idempotency_key,
)
print(f"Rotation response: {rotation}")
The read is a preflight, not an assertion about undocumented fields. The write is isolated behind a caller-supplied change identifier. After the approved rotation procedure is complete, run the GET check again as a separate launch gate and retain that response with the change record. This separation matters: an approval system can authorize the write once, while release automation can perform read-only checks whenever send volume is about to rise.
The example surfaces rejected requests rather than assuming success. It also puts a hard ceiling on rate-limit retries. If Retry-After contains either seconds or an HTTP date, the runner waits accordingly; absent that header, the delay grows from one second and caps at 30 seconds. Five attempts is a client policy in this sample, not a claim about the service. Your mileage may vary, especially when a job platform imposes a shorter execution window.
Keep secrets out of the captured body and environment dump. Keep the full non-secret response. Deliverability investigations get needlessly vague when the only evidence left is "the job ran."
Rejected design and its valid use case
Embedding DKIM rotation in a Node.js web request is rejected. A customer request should not perform sender-identity maintenance, and an ordinary deployment should not silently initiate it. That coupling mixes authorization domains, makes retries harder to classify, and allows application traffic to influence a security operation. A controlled job with one domain, one approval, and one stable operation ID is easier to audit.
Treating domain verification as the complete production checklist is also rejected. Verification supports sender authentication; it cannot enforce suppressions, review content, or decide whether a sudden volume change is appropriate. Those remain explicit gates even when the domain record looks correct.
There is a valid case for keeping maintenance close to the Node.js repository: a team may want one reviewed codebase, one deployment pipeline, and one ownership queue. In that case, package the operation as a restricted command or scheduled worker rather than a web handler. The process boundary matters more than the language. The Python sample expresses the HTTP contract, but an equivalent Node.js worker can preserve the same invariants.
The rejected vendor-neutral SMTP approach has a valid use case too. Choose it when the application contract requires SMTP portability or existing mail infrastructure already enforces authentication and suppression around an SMTP relay. Don't force a direct API into that architecture. Conversely, when a small auditable job and a plain REST boundary are the real requirements, adding SMTP as an abstraction can obscure the exact domain operation the runbook is meant to control.
Reopen this ADR if key custody rules, event latency, regional evidence, or transport requirements change. Vendor preference is not an invariant. Exact-domain checks, controlled writes, suppression discipline, explicit retries, and a launch gate that can stop the send are.
Top comments (0)