An automation pitch can be hard to evaluate when all you can see is a form and a success message. For Review Flow, I wanted the visitor to see what happened next.

The demo puts the business portal on the left and an interactive phone on the right. Start a request, wait for the chosen delay, then open the notification as the customer. The review screen completes the illustrated loop. One person can explore both roles without sending anyone a text.

The frontend uses React, TypeScript, Vite and Tailwind CSS. The source includes an Express backend with optional Twilio integration restricted to an allowlisted number. The deployed Worker version described here runs in simulated mode.

01 / FIELD NOTESMake the handoff visible

The business form gives the visitor a cause; the phone gives them an observable result. Keeping both on the page makes it possible to understand the flow without reading an implementation guide first.

Try it with the fictional request below. You can scrub the clock rather than wait. The review interaction stays local, including its final confirmation.

WATCH THE HANDOFF

Same request. Two perspectives.

Move through a deterministic simulation—not a messaging service.

01 / THE BUSINESS SIDE

A request,
waiting on the clock.

RecipientExample customerModeSIMULATED ONLY
API STATUS IN THIS EXAMPLEpending

The scheduled point has not arrived. No message has been sent.

REVIEW FLOWLOCAL DEMO

00:00

02 / THE CUSTOMER SIDE

The example notification will appear here.

Read the explanation without interacting

The business starts a request. Before the selected delay, its status is pending. At or after the delay, its status is simulated. Opening the illustrated notification reveals a local rating interaction. Every rating follows the same path; no review is published.

Fixed fictional details. No SMS, API request or public review is sent. The local review screen is an illustration, not a third-party service.

The distinction between a simulated message and a delivered one is part of the explanation. The example ends in simulated, not a newly verified delivery receipt.

02 / FIELD NOTESA request identifier that carries the state

The Express version described in the original account keeps pending requests in an in-memory map and changes their status with a timer. The Worker version takes a different route: it puts the request fields and timestamp inside the returned identifier.

The status endpoint decodes that identifier, calculates the scheduled time, and compares it with the current clock:

worker/index.ts · source status handler / TypeScript
const statusMatch = url.pathname.match(/^\/api\/request-status\/(.+)$/);
if (statusMatch) {
  const data = decodeId(statusMatch[1]);
  if (!data) return json({ error: "Request not found" }, 404);
  const sendAt = data.t + data.d * 1000;
  const sent = Date.now() >= sendAt;
  return json({
    id: statusMatch[1],
    name: data.n,
    phone: data.p,
    businessName: data.b,
    delaySeconds: data.d,
    status: sent ? "simulated" : "pending",
    mode: "simulated",
    scheduledAt: data.t,
    ...(sent ? { sentAt: sendAt } : {}),
    messageText: messageFor(data.n, data.b),
  });
}
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

This is the published status-handler excerpt, not a complete server implementation. The decoder and validation around the request are not shown here.

The calculation is small: sendAt is the creation time plus the delay in milliseconds. Before that point, status is pending; afterward, it is simulated. Any instance receiving the same decoded data and clock value can compute the same answer without retrieving a stored request.

The account describes a Base64-encoded payload. Base64 is an encoding, not a privacy boundary: the receiver decodes it to recover the fields. Names and phone numbers should not be treated as protected merely because they are inside an identifier. That clarification is supported by the MDN Base64 reference, not a security audit of the original decoder.

03 / FIELD NOTESPoll until the example reaches a terminal state

The frontend checks status once a second:

src/components/ReviewForm.tsx · source interval / TypeScript
const interval = setInterval(async () => {
  try {
    const res = await fetch(`/api/request-status/${currentRequestId}`);
    if (!res.ok) throw new Error("Failed to fetch status");
    const data = await res.json();
    if (isSubscribed) {
      onStatusUpdate(data);
      if (data.status === "sent" || data.status === "simulated" || data.status === "failed") {
        clearInterval(interval);
      }
    }
  } catch {
    // transient poll failure — next tick retries
  }
}, 1000);
Source excerpt · not executed in this design sample · scroll code horizontally on narrow screens

The shown code stops its interval for sent, simulated or failed, allowing the same response shape to represent different backend modes. A transient polling error is caught and the next tick tries again.

That does not establish every lifecycle detail. This excerpt does not show the surrounding unmount cleanup, request timeout, or a limit on retries. Those are details to inspect in the complete component before drawing a broader reliability conclusion.

It also explains why “the clock has passed the delay” is enough for this demo but not for real SMS. The simulated result is determined by the inputs. A real provider outcome is not.

04 / FIELD NOTESWhat I would change next

The original improvement list starts with status delivery. A real version could push updates rather than asking every second; the write-up proposes server-sent events. That is a next step, not a feature this example demonstrates.

The Express and Worker versions also repeat the API shape by hand. Shared types would make it easier to spot disagreements between them. Finally, an actual business deployment would need its own configured review destination; this portfolio screen only illustrates that step.

The useful result is a demo that can explain itself. You can see which action starts the process, where the next state appears, and which part is still a simulation.

Source, dates, and what this page verifies

This page describes the supplied project account and related full article. Original publication dates remain attached to the source version. No production repository, message provider, Google Sheet, user account or game service was accessed.

The diagrams are newly implemented teaching examples, not screenshots or re-creations of complete application logic. They use fictional data and memory-only state; the CraftNet fragment carries bounded numeric example settings only. Original code excerpts are preserved and are not executed as production code here.

Technical clarifications and source corrections are identified in the article and in docs/batch-3/SOURCE-REVIEW.md. They do not constitute a new application security, performance or deployment audit.

Open the original published source ↗