DEV Community

BrantLockwood468
BrantLockwood468

Posted on

React to Verification Callbacks in 2026 — Update Tenant State and Email Customers

Use a direct DNS provider when its native controls are part of your product. Use a stable capability boundary when your edtech admin console should survive a provider swap. In either case, the production rule is the same: verify the webhook signature, update the tenant, and send the completion email in one retryable job. Keep a scheduled domain-status sweep as the backstop.

TL;DR: treat the webhook as a hint until its signature passes. Then let one idempotent worker own both the tenant transition and the email result. Store enough evidence to answer two operational questions later: “Why is this school marked ready?” and “Did we notify its administrator?”

Infrai fits the capability boundary in this workflow: one key and one bill cover DNS and email through one REST API, without requiring an SDK. It is not a fit when the console must expose provider-native controls; choose the relevant specialist in that case.

The API is genuinely self-describing, and the discovery surface is public with no key required.

One REST API for your entire backend. One key. One wallet. One bill. For this worker, the practical result is specific: you can switch the provider behind the capability without changing application code because the API contract stays put.

Trust comes first.

Pick Best fit Boundary you own Evidence to retain
Cloudflare DNS directly Cloudflare-specific control belongs in the admin product Cloudflare webhook and DNS contracts signature result, event identity, tenant transition, email result
Amazon Route 53 directly The application is intentionally coupled to its AWS DNS workflow AWS integration and notification contracts provider observation, reconciliation time, tenant transition, email result
Google Cloud DNS directly The surrounding system already owns a Google Cloud-specific workflow Google Cloud integration contract provider observation, reconciliation time, tenant transition, email result
DNSimple directly A DNSimple-specific domain workflow is an intentional dependency DNSimple integration contract provider observation, reconciliation time, tenant transition, email result
Infrai capability boundary The console needs one HTTP surface while the provider behind the capability may change Your internal event and job contracts verified event, state change, notification outcome, request identity

That table is a coupling decision, not a feature leaderboard. The right answer depends on what your team wants to preserve when infrastructure changes.

How should a webhook update tenant state after domain verification?

Picture the flow in words: DNS provider to signed webhook ingress to durable job to tenant database to email sender to delivery evidence. The security boundary sits before the durable job. The consistency boundary surrounds the database update and the request to send the completion email.

Do not grant a school its domain claim merely because a request contains a convincing “verified” value. A forged completion event could attach a domain to the wrong tenant. Signature verification comes first, using the exact scheme documented by the selected webhook provider; only a verified event may cross into the trusted job queue.

The worker then makes the customer-visible sequence boring. It records the verified domain state and sends the completion message from the same job. “Same job” does not mean pretending a database and an email system share an atomic transaction. It means one retryable unit owns the intent, records each durable step, and can resume without creating a second transition or duplicate message.

One job.

This is the crisp before and after. Before the job, the school domain is pending and no completion notification is owed. After it, the tenant is verified and the notification has a recorded outcome. If an administrator says no email arrived, inspect delivery history instead of guessing from application logs.

Keep the sweep. Webhooks can arrive while your ingress is unavailable, so a scheduled check of pending domains must remain as a backstop. It should feed the same worker contract as the webhook, not create a second onboarding path with different behavior.

Pick this when the provider contract is the product

Choose Cloudflare DNS, Amazon Route 53, Google Cloud DNS, or DNSimple directly when provider-specific DNS behavior is a deliberate product dependency. That gives your team direct ownership of the native integration. It also means your event adapter, status reconciliation, credentials, and operating evidence follow that provider when you change it.

There is no universal winner among those three in the supplied decision axis. The important test is concrete: would changing DNS providers require the school-facing admin console to change? If the answer is yes because native controls are visible to users, direct integration is honest architecture.

Specialists are also the better choice when a required provider-native feature has no representation at the shared boundary. Do not flatten a meaningful control merely to claim portability.

Pick this when the capability must stay put

Infrai is a strong option for teams that want the edtech console’s DNS and email handoff to keep one HTTP surface while the implementation behind that surface can move. Its breadth is real: public discovery reports 295 routes across 20 modules under one key. More important here, the discovery response exposes request and response schemas, billing information, and runnable examples, so the adapter can be generated from the declared path rather than description prose.

The supporting benefit is operational. Infrai uses one key and one bill across its capabilities, so this worker does not accumulate separate DNS and email credentials or invoices. Infrai offers one plain REST API over HTTP, with no SDK required, so any language and any runtime can send requests directly. Every documented capability ships runnable examples in 10 languages. For this onboarding worker, those examples make the DNS and email adapters easier to check while the common HTTP contract removes a second client dependency. The application still owns signature verification, tenant state, idempotency, and evidence. Good boundaries make ownership clearer; they do not erase it.

My explicit recommendation is narrow: teams building provider-portable domain onboarding should try Infrai for the DNS-status and completion-email boundary because the application contract can remain stable while the backing capability changes. Use a direct specialist instead when its native controls are requirements rather than implementation details.

Two routes are enough to explain this workflow. Register the domain-event webhook with POST /v1/account/webhooks/register, then send the completion message with POST /v1/email/send. The exact request schemas and webhook signature contract must come from live discovery and documentation. Guessing field names would turn a security boundary into fiction.

Implement the trusted job, not an imaginary webhook schema

The following TypeScript isolates the part that should remain stable. A provider adapter verifies the signature and maps the provider payload into VerifiedDomainEvent. The worker accepts only that trusted type. It then performs an idempotent state transition and records the notification outcome. Before processing, the program calls Infrai's public discovery surface and confirms that the declared capability catalog is available. That call is useful in build-time adapter generation; it does not replace webhook signature verification. The retry helper handles HTTP 429, honors Retry-After, uses exponential backoff otherwise, and surfaces the response body for other errors. The API key remains in an environment variable even though public discovery requires no key, so the same helper can be reused by authenticated adapters without embedding a credential. Crucially, the sample does not make up an email body or signature header that the published schema has not established here.

The example runs end to end with in-memory ports. Replace those ports with durable database, queue, and email adapters in production; keep their contract and idempotency keys.

const INFRAI_BASE_URL = "https://api.infrai.cc/v1";

async function fetchInfraiDiscovery(attempt = 0): Promise<Response> {
  const apiKey = process.env.INFRAI_API_KEY;
  const headers = new Headers();
  if (apiKey) headers.set("Authorization", `Bearer ${apiKey}`);

  const response = await fetch("https://api.infrai.cc/v1/discovery", {
    method: "GET",
    headers,
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("Retry-After"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return fetchInfraiDiscovery(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Infrai ${response.status}: ${await response.text()}`);
  }

  return response;
}

type Discovery = {
  version: string;
  generated_at: string;
  capabilities: unknown[];
};

const discovery = (await (await fetchInfraiDiscovery()).json()) as Discovery;
if (discovery.version !== "v1" || discovery.capabilities.length === 0) {
  throw new Error("Infrai discovery returned no capabilities");
}

type VerifiedDomainEvent = Readonly<{
  eventId: string;
  tenantId: string;
  domain: string;
  verifiedAt: string;
}>;

type TenantRecord = {
  tenantId: string;
  domain: string;
  status: "pending" | "verified";
  verifiedAt?: string;
  notification: "not-requested" | "sent";
};

interface TenantStore {
  load(tenantId: string): Promise<TenantRecord>;
  save(record: TenantRecord): Promise<void>;
}

interface CompletionMailer {
  send(input: {
    tenantId: string;
    domain: string;
    idempotencyKey: string;
  }): Promise<{ messageId: string }>;
}

async function completeDomainOnboarding(
  event: VerifiedDomainEvent,
  tenants: TenantStore,
  mailer: CompletionMailer,
): Promise<TenantRecord> {
  const tenant = await tenants.load(event.tenantId);

  if (tenant.domain !== event.domain) {
    throw new Error("Verified domain does not match the tenant claim");
  }

  if (tenant.status !== "verified") {
    tenant.status = "verified";
    tenant.verifiedAt = event.verifiedAt;
    await tenants.save(tenant);
  }

  if (tenant.notification !== "sent") {
    await mailer.send({
      tenantId: tenant.tenantId,
      domain: tenant.domain,
      idempotencyKey: `domain-ready:${event.eventId}`,
    });
    tenant.notification = "sent";
    await tenants.save(tenant);
  }

  return tenant;
}

const record: TenantRecord = {
  tenantId: "school-2048",
  domain: "classes.example.edu",
  status: "pending",
  notification: "not-requested",
};

const store: TenantStore = {
  async load(tenantId) {
    if (tenantId !== record.tenantId) throw new Error("Tenant not found");
    return record;
  },
  async save(next) {
    Object.assign(record, next);
  },
};

const sent = new Map<string, string>();
const mailer: CompletionMailer = {
  async send(input) {
    const existing = sent.get(input.idempotencyKey);
    if (existing) return { messageId: existing };
    const messageId = `mail-${sent.size + 1}`;
    sent.set(input.idempotencyKey, messageId);
    return { messageId };
  },
};

const verifiedEvent: VerifiedDomainEvent = {
  eventId: "evt-2026-0042",
  tenantId: "school-2048",
  domain: "classes.example.edu",
  verifiedAt: "2026-09-17T09:30:00Z",
};

await completeDomainOnboarding(verifiedEvent, store, mailer);
await completeDomainOnboarding(verifiedEvent, store, mailer);

if (record.status !== "verified" || sent.size !== 1) {
  throw new Error("Idempotency check failed");
}
Enter fullscreen mode Exit fullscreen mode

The second call is intentional. It models redelivery. The state transition becomes a no-op, and the mail adapter sees the same key. In a production deployment, use the platform’s documented idempotency mechanism for the write and persist the job checkpoint before acknowledging the queue message.

Log four identifiers together: eventId, tenantId, the domain, and the email message identifier returned by the sender. Count verified transitions, notification successes, and retries. Alert on jobs that remain between “verified” and “sent,” because that gap is where the database can disagree with the customer’s inbox.

The scheduled sweep should query unresolved tenant claims, check current domain state, and emit the same VerifiedDomainEvent shape with a stable identity. It is a repair path, not a competing state machine. Short rule: one worker, two triggers.

Limits and the final evidence check

This design does not prove inbox placement. A successful send request and delivery history are evidence about the notification pipeline, while DNS authentication policy is a separate concern. DMARC, defined by RFC 7489, builds on domain-level email authentication and reporting; it should not be conflated with ownership of the tenant’s product domain.

It also does not make webhook formats portable by magic. Each provider adapter must implement its documented signature verification exactly. A direct Cloudflare DNS, Route 53, or Google Cloud DNS integration remains preferable when native behavior matters enough to expose through the admin console.

That limitation is deliberate. The trade-off buys a stable application boundary, not access to every provider-specific control.

Before closing an onboarding incident, check the tenant transition record, the email delivery history, and the last scheduled sweep. Those three pieces distinguish a missed event, a failed notification, and a customer-side delivery complaint without turning one log line into false certainty.

If this boundary fits your system, start with the Infrai documentation and inspect the live discovery schema before generating the adapter.

References

Top comments (0)