@Mail Domain Check

Next.js | Contact forms | Web3 support | Transactional email

Contact Form Says Sent, but No Request Was Made

A delay followed by a success toast can discard every support request while looking healthy. Prove durable intake before clearing the form, then make email a separately recoverable notification rather than the only copy of the message.

Decision: acknowledge intake, not animation

Browser truth
The submit handler ran, validation passed, and a real HTTP request either left the browser or did not. A spinner, timer, toast and cleared form prove only interface behavior.
Intake truth
The server rejected the request, accepted but did not persist it, or durably stored it under a stable submission reference. Only the final state can support a truthful success response.
Notification truth
An email job is absent, queued, accepted by the provider, accepted by the recipient server, delayed, bounced, failed, suppressed or visible. These states never erase a stored support request.

Browser-local decision tool

Find the first unproved handoff

Select evidence states only. No name, email address, message body, wallet address, credential or support content is requested or transmitted.

Six boundaries to prove

  1. Browser request. In DevTools or an automated test, prove that submit emits one bounded same-origin POST. Disable JavaScript success paths that run without a response.
  2. Server acceptance. Validate allowed fields, lengths and formats server-side; enforce a small body limit, content type, abuse throttling, same-origin policy and the application's CSRF defense.
  3. Durable intake. Commit the request and a stable opaque submission reference before returning success. A database or durable queue record must survive a process restart.
  4. Notification queue. Enqueue one job keyed to the submission reference. A failed email must be retryable without asking the customer to submit again.
  5. Provider outcome. Store a non-secret provider message ID. A successful send API response proves acceptance at that API boundary, not final delivery.
  6. Recipient outcome. Advance from authenticated provider events. Recipient-server acceptance, inbox visibility and human response remain separate evidence.

Never clear the form or show “Message sent” until durable intake succeeds. On timeout with an unknown result, retry the same idempotency key or query the existing submission reference instead of creating a second request.

A minimal same-origin route contract

POST /api/contact
  request: bounded fields + one retry-stable idempotency key
  success: 201 { submissionRef, status: "received" }
  failure: explicit 4xx or 5xx; do not clear the browser form

contact:<submissionRef>
  intake: received | triaged | resolved
  notification: not_queued | queued | api_accepted | delivered
                | delayed | bounced | failed | suppressed

Invariant: success UI requires durable intake, not successful email.

In a Next.js App Router application, a Route Handler can own the HTTP boundary. Keep authorization and validation in the route even when the page already validates, and persist before responding. The browser should receive only the opaque reference and a bounded status, never internal queue details or provider secrets.

Retry without duplicates

  1. Create one opaque idempotency key for the browser submission attempt and reuse it only while retrying an unknown or retryable outcome.
  2. Enforce uniqueness at durable intake. The same key and same normalized payload return the existing submission reference; conflicting reuse is rejected.
  3. Key the business notification job to the submission reference. Keep each actual provider attempt and message ID as child evidence.
  4. Use the provider's idempotency support as an additional boundary, not as a replacement for application-level durable intake.
  5. Authenticate webhook events, make transitions monotonic, and ignore duplicate or out-of-order events that would move evidence backward.

Preserve support privacy

Validate on the server
Allow only expected fields and bounded lengths. Reject unexpected content types, oversized bodies and malformed values before persistence or notification.
Limit abuse
Use layered rate limits, bot defenses appropriate to risk, origin checks and CSRF controls. Do not leak whether a private account, wallet claim or dispute already exists.
Log references, not messages
Routine logs and analytics should carry the submission reference, bounded state, route, latency and error class. Keep names, addresses, wallet data, message bodies, credentials and provider payload secrets out.
Retain deliberately
Encrypt sensitive stored content, restrict access, define deletion and audit policy, and avoid making an inbox the only searchable case system.

What the current public implementation proves

Hybrid Agent issue 7 reports that a contact form displays a successful submission while messages are never sent. In the reviewed main revision, frontend/app/Contact/page.jsx validates required fields, sets a submitting state, waits through setTimeout, clears the form and displays success. The handler contains no HTTP request or durable intake boundary.

This is especially consequential for a Web3 escrow interface: bug reports, disputes, wallet claims and support requests can disappear while the customer receives positive confirmation. Adding a mail SDK directly to the browser would still leave secrets, abuse controls, persistence and retry semantics unresolved. Put the trust boundary on the server and keep intake independent from notification delivery.

Acceptance tests before release

  1. Block or fail the POST and confirm the form remains populated, the success state stays hidden, and the user receives a bounded retry message.
  2. Restart the server after a successful response and prove the submission reference still resolves to the stored request.
  3. Submit twice with the same idempotency key and prove that only one contact record and one business notification job exist.
  4. Make the email provider fail after intake and prove the customer receipt remains valid while the notification job becomes retryable.
  5. Replay authenticated delivery events out of order and prove that final evidence never regresses.
  6. Inspect logs and analytics to confirm that message bodies, names, addresses, wallet data and credentials are absent.
Verify the notification sender independently

Check the public SPF, DKIM, DMARC, PTR and MTA-STS surface without entering a recipient or support message.

Check sending domain

Primary references