The lowest-integration design is not the one with the fewest DNS records; it is the one that keeps password-reset delivery available while DNS ownership, authentication, and key rotation change independently. For a marketplace running a Node.js sender, use a separate transactional subdomain, publish SPF and DKIM there, verify DMARC alignment before enabling traffic, and rotate DKIM with overlapping selectors rather than replacing a live key in place. This keeps a control-plane change out of the synchronous reset path.
Short answer: treat custom sending domain setup as a versioned deployment, keep the Node.js request path unaware of DNS propagation, and allow old and new DKIM selectors to coexist until verification and rollback windows have closed.
How can Node.js keep custom sending domain setup outside the reset API?
Draw the integration boundary before choosing an API. The reset handler should validate the account-recovery request, create the single-use token, enqueue a message against a domain configuration already marked ready, and return. It should never wait for a DNS lookup or attempt domain setup. A separate control plane verifies ownership and the authentication state that matters to the message: the visible From domain must align with at least one authenticated identifier under the domain's DMARC policy. SPF authenticates the domain used by the SMTP path, while DKIM authenticates a signing domain; DMARC evaluates alignment with the visible From domain. Publishing all three acronyms is not proof that they line up.
For a marketplace reset flow, I would isolate mail at a name such as auth.example.test instead of sharing the organizational domain with newsletters or seller campaigns. That is a capacity-planning decision as much as a security decision: a reset message with a short lifetime has a different SLO and failure budget from bulk mail, and coupling their reputation, DNS changes, retry queues, and operational ownership creates a failure domain much larger than the feature needs. The exact expiry is a product choice; what matters operationally is that queue delay, provider acceptance, and inbox delivery consume that budget before a user can act.
The control plane should refuse activation until it can answer four questions: does the tenant control the requested name, is the expected SPF authorization present without creating multiple SPF policy records, is at least one DKIM selector retrievable with the expected public key, and does the DMARC policy parse with an alignment mode the sender can satisfy? A DNS lookup succeeding once is weak evidence. Cache the observation with its resolver, timestamp, and record digest, then require repeated observations from the verification job before moving the domain state from pending to ready. I'm not sure one global observation interval fits every DNS operator; authoritative TTLs and the platform's measured propagation distribution should set that interval.
Keep the states explicit: pending, ready, rotating, and retiring.
Observe SPF, DKIM, and DMARC as deployment state
Consider a bounded incident review rather than an invented success story. I initially model the risky event as "the new key is wrong." The wider failure is a non-atomic deployment: the signer begins using selector s2026b before enough resolvers can retrieve its public key, or the old s2026a record is removed while accepted messages bearing that signature are still being evaluated. Recipients then see a message that was signed correctly at send time but cannot be verified from their current DNS view. The fix is architectural, not a faster retry loop.
This is the invariant: adding authentication material is reversible; removing it is destructive. Publish the new selector, observe it, sign a controlled stream with it, expand traffic, stop creating signatures with the old selector, and only then retire the old public key after the defined retention window. Rollback during expansion means selecting the old private key again, not editing DNS under pressure.
Short messages make this easy to underestimate. A password-reset email may expire quickly, but mail systems can queue or defer it and recipients can evaluate authentication after the application token has expired. Token validity and DKIM-key retirement are separate clocks.
Don't derive one from the other.
Activate the verified DKIM selector with compare-and-swap
The Node.js service should call an internal signing boundary or mail adapter, not perform DNS mutation during a password-reset request. The controller below is written in Go and shows the safety property: it promotes a selector only after a verifier has observed the expected record, retains the previous selector for rollback, and makes activation idempotent. The interfaces deliberately omit any vendor route because DNS and mail APIs vary; the state transition is the portable part.
package rotation
import (
"context"
"errors"
"fmt"
)
type State struct {
Domain string
ActiveSelector string
PreviousSelector string
Candidate string
CandidateDigest string
Verified bool
}
type Store interface {
Load(ctx context.Context, domain string) (State, error)
CompareAndSwap(ctx context.Context, before, after State) error
}
type DNSVerifier interface {
ObservesDKIM(ctx context.Context, domain, selector, digest string) (bool, error)
}
type Controller struct {
Store Store
Verifier DNSVerifier
}
func (c Controller) VerifyAndActivate(ctx context.Context, domain string) (State, error) {
before, err := c.Store.Load(ctx, domain)
if err != nil {
return State{}, fmt.Errorf("load rotation state: %w", err)
}
if before.Candidate == "" || before.CandidateDigest == "" {
return State{}, errors.New("rotation candidate is incomplete")
}
observed, err := c.Verifier.ObservesDKIM(
ctx, before.Domain, before.Candidate, before.CandidateDigest,
)
if err != nil {
return State{}, fmt.Errorf("verify candidate selector: %w", err)
}
if !observed {
return before, nil // A later reconciliation pass checks again.
}
after := before
after.PreviousSelector = before.ActiveSelector
after.ActiveSelector = before.Candidate
after.Verified = true
if err := c.Store.CompareAndSwap(ctx, before, after); err != nil {
return State{}, fmt.Errorf("activate verified selector: %w", err)
}
return after, nil
}
An HTTP 202 Accepted from a domain-setup endpoint should mean only that desired state was accepted. Rotation completion belongs to an asynchronous reconciliation loop with a durable operation ID. The Node.js application can poll or consume an event, but its reset handler should continue using the last ready configuration. Make concurrent requests converge on the same candidate selector and digest; otherwise two deploys can each pass verification and still race during activation. A compare-and-swap conflict can return 409 Conflict to the controller and trigger a fresh read without changing the active signer.
Test the state machine without public DNS first. Feed the verifier stale, missing, and then matching observations; race two activation attempts; cancel the context between verification and compare-and-swap; and confirm that the old selector remains usable after every interrupted transition. Then run a canary against a delegated test subdomain.
No heroics.
Measure the overlap window before retiring a key
The overlap window is an operating interval, not an arbitrary sleep. Track verification disagreement across resolvers, signing volume by selector, DMARC pass and alignment rates, reset-queue age, and the fraction of reset tokens that expire before the delivery pipeline accepts the message. Those signals separate DNS observation lag from sender backlog. Zero newly signed traffic on the previous selector is necessary before retirement, although the retention window still has to pass.
Rehearse the sequence with a delegated test subdomain: publish a candidate, observe it from the verifier's normal resolver perspectives, shift a canary, stop new signatures on the prior selector, and exercise rollback before deletion is allowed. A useful release gate is boring — desired records rendered, ownership observed, SPF policy parsed, DKIM candidate observed, DMARC alignment checked, canary signed, rollback selector retained, and dashboards receiving selector-level data. Record the exact configuration digest approved at each gate.
For capacity, forecast verification work from domain onboarding and rotation frequency, not message volume; forecast signing and delivery from peak reset demand, not daily averages. Separate queues and SLOs follow from those different scaling variables. This distinction also keeps a DNS slowdown from consuming workers needed to send for already-ready domains.
Charge integration effort to its real owner
Integration effort has at least three components: application code, operational ownership, and exit cost. Counting API calls hides the second and third.
| Approach | Application integration | On-call surface | Lock-in and control | Poor fit |
|---|---|---|---|---|
| Managed domain and signing control plane | Small adapter plus asynchronous status handling | External API, DNS, and delivery signals still need runbooks | Faster initial setup; state and event semantics may be provider-specific | Teams requiring private-key custody or custom rotation policy |
| Self-hosted signing and verification | Signer boundary, key storage, reconciler, audit log, and DNS adapter | Full key lifecycle and delivery diagnosis stay with the platform team | Maximum policy control and a larger maintenance burden | Small teams without cryptographic operations coverage |
| Split model over replaceable adapters | Stable application contract plus provider adapters | Internal controller and external dependencies share the page | Moderate initial effort, clearer migration boundary | A single-domain system with no realistic portability requirement |
The catch is that a split control plane is not suitable when the platform team cannot own reconciliation and state migration. In that case, use a managed control plane and keep the application adapter narrow. Stay self-hosted when private-key custody, audit requirements, or signer placement are hard constraints, but budget for rotation drills and after-hours ownership rather than treating package installation as the completed system.
If DNS changes after activation, reconciliation should demote the domain for new traffic according to policy while preserving an audit trail. That response belongs to the owner named in the table, not to whichever application engineer happened to ship the reset form.
Rotation does not solve compromised application credentials, stolen reset tokens, mailbox forwarding behavior, or abuse of the reset endpoint. NIST's authenticator guidance treats recovery flows as security-sensitive; rate limits, single-use tokens, protected token storage, and an expiry appropriate to the threat model remain application responsibilities. Email authentication establishes domain-level signals. It does not make the message content or the account-recovery flow trustworthy by itself.
This advice also stops being the right abstraction for a tiny internal system that never sends from a custom domain: adding a reconciler can create more on-call load than it removes. At the other extreme, a regulated platform may need hardware-backed key custody, formal dual control, and evidence retention beyond this state machine. Use the overlap pattern, but let the security policy determine who can create, activate, and retire keys.
The decision rule is plain: choose the least elaborate control plane that can prove alignment, preserve a known-good selector during rotation, and keep DNS work outside the password-reset request path. Everything else is an ownership choice.
Top comments (0)