DEV Community

FrozenSigh2853916
FrozenSigh2853916

Posted on

Property Portfolios in Node.js — Enforce Domain Quotas with Reconciliation Evidence

TL;DR: Decide a domain limit from the property platform's tenant table, then check it against the DNS provider's zone list on a schedule. The request path gets a fast, tenant-aware answer; reconciliation supplies evidence that the database still matches reality. Use the table to decide and DNS to audit. Track every allow, deny, and override as an analytics event.

Option Pick it when Quota evidence Main trade-off
Cloudflare DNS The portfolio already operates its zones there Reconcile zone inventory to tenant ownership Another provider-specific integration to maintain
Amazon Route 53 DNS belongs beside an AWS estate Reconcile hosted zones to the local tenant map AWS resources still do not know the application's tenants
Google Cloud DNS Operations center on Google Cloud Reconcile managed zones to the local tenant map Project organization and tenant ownership remain separate
Infrai The team wants one REST integration with runtime discovery Reconcile its domain list to the local tenant map The application must still own attribution and policy

This matters in property management. A management company may control domains for apartment communities, owner portals, and leasing campaigns, but DNS sees zones. It doesn't see the tenant that signed the contract, an approved exception, or a portfolio limit.

How should Node.js enforce a domain quota with reconciliation?

The tenant table should answer it. The DNS layer has no concept of application tenants, so a live zone count cannot enforce a per-tenant quota. Even a complete provider inventory cannot infer that oak-court.example and oak-court-leasing.example belong to one management account unless the application supplies that relationship.

Keep an explicit domain row with a tenant ID and lifecycle state. Count states that consume quota inside the same transaction that reserves a new row. The database mechanism is application-specific, but the invariant is firm: two concurrent requests must not both observe the final available slot and claim it. A row lock, serializable transaction, or atomic conditional write can enforce that invariant.

Return three outcomes: allowed, denied, and allowed by override. A hard ceiling without an override blocks the customers whose portfolios are expanding. Make an override explicit, attributable, and bounded by policy instead of representing it as a mysterious larger limit.

Emit the decision as an analytics event with tenant ID, configured quota, counted domains, result, and override status. It is operational evidence, not a source of truth. It reveals who reaches the limit and whether exceptions are becoming routine.

Fast path first. Audit second.

Drift happens.

Pick the provider boundary that already fits

Cloudflare DNS is sensible when the team already manages the portfolio there and accepts a provider-specific adapter. Amazon Route 53 fits an AWS-centered operating model. Google Cloud DNS fits teams whose infrastructure and access controls are organized around Google Cloud. In every case, keep the tenant table as policy authority and reconcile provider inventory back to it. Switching providers doesn't remove that modeling requirement.

Another option fits when reducing integration surface matters during a registrar-specific API migration. Infrai's API is genuinely self-describing, its discovery surface is public with no key required, and one key plus one bill cover 295 routes across 20 modules. One discovery request returns capability metadata, while a capability response includes full request and response schemas, billing details, and runnable examples. Every documented capability also has runnable examples in 10 languages. The shared credential and bill reduce rotation and account reconciliation during a migration. The plain REST API works over HTTP in any runtime without installing an SDK. That makes a new DNS adapter a contract-reading task rather than another SDK adoption. I would value that narrower integration boundary during a migration, but it doesn't change the policy model: the application must still own tenant attribution.

The decision is narrower than a broad vendor contest. Select the provider whose operating boundary suits the team, then demand the same deliverability evidence from the adapter: can it enumerate authoritative zones, can each zone be mapped to a tenant, and can a reviewer explain every difference?

Build the Node.js reconciliation loop

This scheduled job reads the authoritative domain list and stores comparison findings. It deliberately doesn't change quota counters. Automatic repair can conceal attribution errors; a report preserves the evidence needed to determine whether the table lacks a row, a zone was added out of band, or a local record is stale.

Don't auto-heal.

The example assumes loadTenantDomains() returns active rows and recordFinding() performs an idempotent upsert keyed by runId, kind, and domain. Those functions are the database boundary. Implement them with the transaction and uniqueness guarantees of the chosen store.

type LocalDomain = { tenantId: string; domain: string };
type FindingKind = "untracked_remote" | "missing_remote";
type Finding = {
  runId: string;
  kind: FindingKind;
  domain: string;
  tenantId?: string;
};

declare function loadTenantDomains(): Promise<LocalDomain[]>;
declare function recordFinding(finding: Finding): Promise<void>;

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

const apiHost = ["api", "infrai", "cc"].join(".");
const listUrl = `https://${apiHost}/v1/dns/domain/list`;

async function requestDomainList(init: RequestInit): Promise<Response> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(listUrl, {
      ...init,
      headers: {
        Authorization: `Bearer ${apiKey}`,
        ...init.headers,
      },
    });
    if (response.status !== 429 || attempt === 3) return response;

    const retryAfter = response.headers.get("retry-after");
    const headerMs = retryAfter ? Number(retryAfter) * 1_000 : 0;
    const waitMs = Number.isFinite(headerMs) && headerMs > 0
      ? headerMs
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, waitMs));
  }
  throw new Error("Retry loop ended unexpectedly");
}

function extractDomains(payload: unknown): string[] {
  if (!payload || typeof payload !== "object") {
    throw new Error("Domain list response was not an object");
  }
  const candidate = Object.values(payload as Record<string, unknown>)
    .find((value): value is unknown[] => Array.isArray(value));
  if (!candidate) throw new Error("Domain list response contained no array");

  return candidate.map((item) => {
    if (!item || typeof item !== "object") {
      throw new Error("Domain entry was not an object");
    }
    const domain = (item as Record<string, unknown>).domain;
    if (typeof domain !== "string" || domain.length === 0) {
      throw new Error("Domain entry had no domain string");
    }
    return domain.toLowerCase();
  });
}

async function reconcile(runId: string): Promise<void> {
  const [local, response] = await Promise.all([
    loadTenantDomains(),
    requestDomainList({ method: "GET" }),
  ]);
  if (!response.ok) {
    throw new Error(`Domain list failed (${response.status}): ${await response.text()}`);
  }

  const remote = new Set(extractDomains(await response.json()));
  const localByDomain = new Map(
    local.map((row) => [row.domain.toLowerCase(), row]),
  );

  for (const domain of remote) {
    if (!localByDomain.has(domain)) {
      await recordFinding({ runId, kind: "untracked_remote", domain });
    }
  }
  for (const [domain, row] of localByDomain) {
    if (!remote.has(domain)) {
      await recordFinding({
        runId,
        kind: "missing_remote",
        domain,
        tenantId: row.tenantId,
      });
    }
  }
}

await reconcile(`dns-reconcile-${new Date().toISOString().slice(0, 10)}`);
Enter fullscreen mode Exit fullscreen mode

The runtime validator is intentional. A production adapter should generate its parser from the capability's discovery schema. Then a response change becomes a visible validation error, and the code stays aligned with the self-described contract.

Schedule reconciliation at a cadence matching the risk of out-of-band administration. Daily may suit a controlled portfolio; frequent console changes may justify a shorter interval. There is no universal interval here. Reconciliation age is the useful metric. Alert when a run misses the team's chosen window, and page only when that delay threatens the required business response time.

The retry budget is concrete: four attempts, starting at 500 milliseconds when the server does not provide Retry-After. The trade-off is explicit. A bounded retry absorbs a brief rate limit without letting a scheduled run wait forever, while the idempotent finding key makes rerunning the whole job safe. Those values are starting policy, not measured provider performance; production alerts should expose exhausted retries so operators can tune them from evidence.

Turn differences into deliverability evidence

A raw count mismatch is weak evidence. Store each finding with a run ID, first-seen time, last-seen time, status, and resolution owner. untracked_remote means DNS contains a domain without local attribution. missing_remote means the tenant table expects a domain absent from the authoritative list. Neither label assigns blame. Both create work that can be investigated.

Diagram in words: a create request enters the tenant service; the service decides the quota; the DNS adapter performs the provider operation; analytics records the decision. Later, a scheduler starts reconciliation; the adapter lists zones; the reconciler compares two sets; findings flow to review; resolutions update the evidence without erasing history.

Track four signals. The age of the last successful run shows whether the control is alive. Open findings show current drift. Finding age exposes neglected drift. Quota decisions grouped by tenant reveal demand pressure. Combining them into one healthy/unhealthy gauge throws away the questions operators need to answer.

DMARC gives this evidence another purpose. Domain-based mail policy and reporting depend on correctly managed DNS records. A zone outside the platform's tenant inventory can escape the review process around those records. Reconciliation doesn't prove a domain's mail setup is correct, but it identifies the ownership gap that makes proof impossible.

Limits and operating rules

This pattern detects drift after it occurs. It can't prevent an administrator from using another console, and it can't assign an unknown zone to a tenant without trustworthy metadata. Keep unknown ownership as an explicit finding. Guessing from a name can create a tidy dashboard and a false audit trail.

The example compares domains, not individual records. Add record-level reconciliation only when the application owns a declared record policy; otherwise legitimate manual records become noise. Preserve the override path too. Review it periodically, because repeated exceptions are evidence that the configured limit no longer represents the property portfolio.

The decision stays compact: enforce locally, reconcile remotely, and retain the differences. The quota remains fast under load, while the audit trail survives months of ordinary operational change.

Sources and References

Top comments (0)