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.
- Submission state
- Notification state
- Next action
- Safety rule
Six boundaries to prove
- 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.
- 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.
- 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.
- Notification queue. Enqueue one job keyed to the submission reference. A failed email must be retryable without asking the customer to submit again.
- Provider outcome. Store a non-secret provider message ID. A successful send API response proves acceptance at that API boundary, not final delivery.
- 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
- Create one opaque idempotency key for the browser submission attempt and reuse it only while retrying an unknown or retryable outcome.
- Enforce uniqueness at durable intake. The same key and same normalized payload return the existing submission reference; conflicting reuse is rejected.
- Key the business notification job to the submission reference. Keep each actual provider attempt and message ID as child evidence.
- Use the provider's idempotency support as an additional boundary, not as a replacement for application-level durable intake.
- 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
- Block or fail the POST and confirm the form remains populated, the success state stays hidden, and the user receives a bounded retry message.
- Restart the server after a successful response and prove the submission reference still resolves to the stored request.
- Submit twice with the same idempotency key and prove that only one contact record and one business notification job exist.
- Make the email provider fail after intake and prove the customer receipt remains valid while the notification job becomes retryable.
- Replay authenticated delivery events out of order and prove that final evidence never regresses.
- Inspect logs and analytics to confirm that message bodies, names, addresses, wallet data and credentials are absent.
Check the public SPF, DKIM, DMARC, PTR and MTA-STS surface without entering a recipient or support message.
Primary references
- Next.js backend-for-frontend guide and Route Handler boundary
- Next.js Route Handler request and response behavior
- Resend send API response and idempotency-key boundary
- Resend sent, delivered, delayed, bounced, failed and suppressed event meanings
- OWASP server-side input validation guidance
- OWASP CSRF prevention guidance
- OWASP security logging and data-exclusion guidance
- Public contact-form false-success report
- Source revision reviewed for the public implementation boundary