DEV Community

ChrysostomHayes8537
ChrysostomHayes8537

Posted on

A Node.js Guide to SPF DKIM and DMARC Tenant Alignment

Short answer: treat SPF, DKIM, and DMARC as one identity decision for each message, then manage the DNS records as versioned outputs of that decision. For an edtech platform giving every school its own subdomain, the least complex useful design is one declared mail identity per tenant, one controlled publishing path, and a reconciler that compares intent with public DNS. DMARC passes when an authenticated SPF or DKIM domain aligns with the visible From domain; publishing three records does not by itself create that alignment.

This changes the unit of work. The unit is not a TXT record. It is a tenant sender identity that must remain coherent while domains, signing selectors, and delivery infrastructure change.

How should SPF, DKIM, and DMARC work as one system?

SPF authenticates the domain used by the message's SMTP envelope path. DKIM authenticates a signing domain carried in the message signature. DMARC evaluates those results against the domain in the visible From header, the identity a recipient actually sees. Under DMARC, either an aligned SPF pass or an aligned DKIM pass is sufficient; both paths do not have to pass.

Suppose a school sends a course reminder as teacher@oak.example.edu. The delivery system could produce an SPF pass for a separate bounce domain while DKIM signs with another organizational domain. Each underlying mechanism may report success, yet neither authenticated domain necessarily aligns with oak.example.edu. DMARC can therefore fail. The surprising part is logical, not syntactic: three independently valid DNS entries do not prove that one message uses a consistent identity.

Alignment may be relaxed or strict. RFC 7489 defines relaxed alignment in terms of sharing an Organizational Domain and strict alignment as an exact domain match. A tenant model should choose that policy deliberately. Strict alignment creates a clearer boundary for independently managed school subdomains, but it also makes renames and delegation changes less forgiving. Relaxed alignment tolerates some parent/subdomain variation, which can simplify operations but may express a wider trust boundary than the product intends.

That is the real trade-off.

Three records. One verdict.

Model intent before touching DNS

For this system, I would store a desired identity document rather than three unrelated record rows. It should answer four questions: what users see in From, what domain receives bounces, what domain signs, and what DMARC alignment mode applies. The publisher can derive records from that state, while the verifier tests the public result as a connected system.

A compact model might look like this:

type AlignmentMode = "relaxed" | "strict";

type TenantMailIdentity = {
  tenantId: string;
  fromDomain: string;
  envelopeDomain: string;
  dkimDomain: string;
  dkimSelector: string;
  alignment: { spf: AlignmentMode; dkim: AlignmentMode };
  revision: number;
};

const oakSchool: TenantMailIdentity = {
  tenantId: "school_oak",
  fromDomain: "oak.example.edu",
  envelopeDomain: "bounce.oak.example.edu",
  dkimDomain: "oak.example.edu",
  dkimSelector: "mail1",
  alignment: { spf: "relaxed", dkim: "strict" },
  revision: 12
};
Enter fullscreen mode Exit fullscreen mode

The revision matters because DNS publishing is asynchronous from the application's point of view. A worker might be reconciling revision 11 while an administrator saves revision 12. Without an explicit revision, an older job can overwrite newer intent or mark a tenant ready after checking stale names. That failure looks like random authentication trouble downstream, but it began as ordinary distributed-state drift.

Keep secret signing material outside this document. The identity model needs a stable reference and public-key publication state, not a private key copied through queues or logs. Also keep message authorization separate from DNS ownership: proving control of a school subdomain is a prerequisite for publishing, not evidence that every application process should be allowed to send as it.

A minimal Node.js alignment check

The following TypeScript checks the core domain relationship before any record is published. It intentionally does not attempt to discover the Organizational Domain because that requires a current public-suffix data source, which is outside the supplied standard. Instead, the caller provides the relevant organizational domains after resolving them through its chosen maintained data path. This keeps the decision visible and testable.

type Mode = "relaxed" | "strict";

function normalizeDomain(value: string): string {
  return value.trim().toLowerCase().replace(/\.$/, "");
}

function aligned(
  authenticatedDomain: string,
  fromDomain: string,
  mode: Mode,
  organizationalDomain: (domain: string) => string
): boolean {
  const authenticated = normalizeDomain(authenticatedDomain);
  const visible = normalizeDomain(fromDomain);

  if (mode === "strict") return authenticated === visible;
  return organizationalDomain(authenticated) === organizationalDomain(visible);
}

type AuthenticationResult = {
  spfPass: boolean;
  spfDomain: string;
  dkimPass: boolean;
  dkimDomain: string;
};

function dmarcPasses(
  identity: TenantMailIdentity,
  result: AuthenticationResult,
  organizationalDomain: (domain: string) => string
): boolean {
  const spfAligned = result.spfPass && aligned(
    result.spfDomain,
    identity.fromDomain,
    identity.alignment.spf,
    organizationalDomain
  );
  const dkimAligned = result.dkimPass && aligned(
    result.dkimDomain,
    identity.fromDomain,
    identity.alignment.dkim,
    organizationalDomain
  );

  return spfAligned || dkimAligned;
}
Enter fullscreen mode Exit fullscreen mode

This is a preflight, not a replacement for observing actual authentication results. It catches obvious intent errors cheaply: a strict DKIM policy paired with a different signing domain, or an envelope identity that cannot meet the selected SPF alignment rule. It also makes the OR relationship explicit. Teams sometimes build dashboards that turn red unless both mechanisms align, which is stricter than the DMARC pass condition and obscures which path is meant to carry authentication during a migration.

Reconcile intent with what the world can query

A publisher should move each tenant through explicit states such as pending ownership, publishing, verifying, and active. Activation belongs after authoritative public answers match the current revision. A successful control-plane write alone does not establish that the intended records are now the answers observed by receivers.

For each reconciliation cycle, load the newest tenant revision, derive the expected names and values, query DNS independently, and compare normalized answers. Before recording success, load the revision again. If it changed, discard the observation and retry from the new intent. This small optimistic-concurrency check prevents a stale verifier from blessing a superseded configuration.

Records lag.

Do not reduce the comparison to "record exists." Check that the expected selector belongs to the current DKIM revision, that the policy is attached to the intended From domain, and that the envelope and signing domains still satisfy their declared alignment modes. Extra or old records deserve separate reporting because they may be harmless during a staged rotation but dangerous if the application can still select them.

Observability should follow the same identity key. Logs and metrics need the tenant ID, intent revision, From domain, selector, observed DNS state, and the SPF-aligned and DKIM-aligned outcomes as separate fields. Avoid storing message content. A useful alert says revision 12 has remained different from published DNS across repeated checks; a vague "DMARC broken" alert sends an operator toward three record screens without showing which relationship drifted.

Cost control fits naturally here. Cache unchanged DNS observations for a bounded reconciliation interval, back off repeated checks while publication is pending, and trigger immediate verification on an intent revision. Do not poll every tenant at the same high frequency forever. The expensive mistake is usually uncontrolled background work, not the handful of comparisons needed for a domain change.

Operate changes as identity migrations

DKIM selector rotation is the cleanest example. Publish the new public key, verify it, begin signing with the new selector, and retain the old public key while messages bearing old signatures may still be evaluated. Only then retire the old selector from intended state. The exact overlap depends on the system's message lifetime and operational policy, so inventing a universal number would be false precision.

Domain changes need the same staged thinking. If a school moves from one tenant subdomain to another, create and verify the new identity before allowing it in From. Keep the application from mixing a new From domain with an old signing or envelope configuration during the transition. One atomic intent revision should select the complete tuple.

The operational checklist is short in practice, but sequence matters. Confirm delegated ownership first. Review the visible From, envelope, and DKIM domains together, then verify the selected strict or relaxed relationships. Publish from one current revision and read the records back independently. Send controlled test mail through the real path, inspect authentication outcomes, and keep the tenant inactive when neither aligned path passes. During later changes, preserve the last verified identity until its replacement has been observed and tested. Finally, watch drift continuously; onboarding success is not permanent proof.

This design is deliberately boring. It spends engineering effort on revisioned intent, independent observation, and small state transitions instead of hiding three loosely related forms behind a green status badge. For a solo team shipping tenant email, that is a good bargain: fewer ambiguous failures, bounded reconciliation work, and no dependency on one delivery provider's configuration vocabulary.

Further reading

Top comments (0)