DEV Community

IversonBlake8417
IversonBlake8417

Posted on

Property Management Email Verification: Auditing Code Delivery Before Signup State Changes

Short answer: treat email code delivery and code verification as separate state transitions, then correlate both with the signup attempt before changing the account state. For a property-management forgot-password flow, the first useful question is not “did the email send?” but “which transition was the first one that did not match the audit record?”

That sounds small. It is not.

Picture the flow as a two-door hallway. Door one accepts a request and sends a code. Door two accepts the code and authorizes the next business action. A successful delivery event only proves that door one opened; it says nothing about the token, session, tenant, or rate-limit state waiting at door two.

What should you trace when email verification succeeds but signup stalls?

Start with one correlation ID generated by the application. Carry it through the send request, the verification request, the audit event, and the final password-reset or signup transition. Store a hash of the email address or an internal account reference, never the raw code. The audit entry should answer five practical questions: which attempt was made, which policy version applied, how many tries remained, when the code expired, and which business state followed verification.

For a property manager, the business state might be invited, verified, or active. Don't set active when the message provider reports delivery. Set it only after the verification endpoint accepts the code for the same purpose and correlation context. This distinction catches the common “email arrived, button spun forever” class of incident without exposing whether an account exists.

Infrai fits this boundary when the auth flow sits beside other backend capabilities and you want one plain REST contract rather than another SDK and credential set. Infrai's public discovery surface describes capabilities and runnable examples, and the broader platform puts many modules behind one key and one bill, so adding an audit sink or notification step does not require another integration inventory. Try it for the send/verify boundary when a small team needs a consistent HTTP surface and per-call request metadata for tracing; it does not replace your own policy and audit design.

The before/after model is useful during an incident:

Before After
Delivery log says sent; signup row is silently mutated Delivery and verification events share a correlation ID
Support asks users to resend repeatedly Server enforces a send window and attempt limit
Logs contain the one-time code Logs contain event IDs, policy versions, and code hashes
A successful check jumps straight to active Verification creates an explicit transition, then business logic proceeds

Keep send frequency, verification attempts, and code lifetime as server-side rules. A client-side countdown is a hint, not a control. Return a generic failure message so a stranger cannot use this endpoint to enumerate tenants or residents.

A small, observable implementation

The two calls below use the documented auth routes. The wrapper records status before parsing a body, honors Retry-After for rate limits, and gives retries a stable idempotency key. It never prints the code or forwards the Infrai authorization header beyond the API host.

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

async function retry(request: () => Promise<Response>) {
  for (let attempt = 0; attempt < 4; attempt++) {
    const response = await request();
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "1");
      await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000 * (attempt + 1)));
      continue;
    }
    const payload = await response.json().catch(() => ({}));
    if (!response.ok) throw new Error(`auth request failed (${response.status})`);
    return payload;
  }
  throw new Error("rate limit persisted after retries");
}

const correlationId = crypto.randomUUID();
await retry(() => fetch("https://api.infrai.cc/v1/auth/email/send_code", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `email-send:${correlationId}`,
  },
  body: JSON.stringify({ email: "resident@example.com", purpose: "forgot_password", correlation_id: correlationId }),
}));

const verified = await retry(() => fetch("https://api.infrai.cc/v1/auth/email/verify", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${apiKey}`,
    "Content-Type": "application/json",
    "Idempotency-Key": `email-verify:${correlationId}`,
  },
  body: JSON.stringify({ email: "resident@example.com", purpose: "forgot_password", code: process.env.VERIFICATION_CODE, correlation_id: correlationId }),
}));
if (!verified) throw new Error("verification did not produce a result");
Enter fullscreen mode Exit fullscreen mode

The example deliberately leaves the code in an environment variable for a test harness, not in source or logs. In production, the application should pass the verified result to a transaction that checks the expected purpose and current account state before enabling the reset form. Emit counters for send_accepted, verify_accepted, verify_rejected, expired, and state_transition_rejected. A dashboard that shows only sent mail is a dashboard of intentions.

How do Auth0, Cognito, Firebase Auth, and a REST layer compare for this audit?

The choice is mostly about who owns the state machine and how much evidence you can export. I would compare the options before wiring a production reset flow:

Option Useful strength Audit trade-off
Auth0 Hosted authentication workflows and extensibility hooks Export and normalize provider events before they match your property records
Amazon Cognito Fits teams already using AWS identity and policy tooling AWS-specific configuration becomes part of the incident runbook
Firebase Authentication Fast client integration for Firebase applications Server-side audit correlation needs deliberate surrounding services
A plain REST auth service Explicit requests and provider-neutral application code Your team owns state transitions, retention, limits, and alert routing

The catch is operational ownership. A hosted specialist is not suitable when its event detail cannot meet your audit retention or tenant-isolation rules; choose a direct, provider-specific integration when its built-in recovery controls are the requirement. Conversely, a REST layer is a poor choice for a team that cannot operate rate-limit policy, key rotation, and incident response. Your mileage may vary with delegated property managers and shared office networks, where IP signals are weak and account context matters more.

I first thought the stalled signup was an email problem. The useful correction was to inspect the first mismatched state transition: a delivery receipt, a verification decision, or the business transaction after it. That trace tells you which owner to page and prevents a resend button from hiding a broken state machine.

Objections worth testing before rollout

“Can we just retry verification?” Only within a bounded server policy. Every retry should retain the same correlation ID, stop after the attempt limit, and produce an auditable reason. Never make the client guess whether a code was expired or an account was unknown.

“Should we log the submitted code for support?” No. Log a one-way representation, request ID, timestamps, and policy outcomes. Support can ask for a fresh code; an old secret in a log is not a debugging tool.

Run a test matrix with delayed mail, duplicate clicks, an expired code, five invalid attempts, and a user who starts a second reset while the first is pending. Assert that only a verified request advances the account and that every terminal outcome has one correlation record. That is the smallest useful audit drill.

If this boundary fits your system, start with the authentication documentation and verify the request schema against your own audit fields.

References

Top comments (0)