Pre-built code blocks

Ask customers for a public review

Exit interview + review request — ask everyone, route by platform

For when the answer to “Ask finished customers for a public review (Google, Facebook, BBB)” is yes.

The step after a job is finished: a short interview, then the customer's OWN words offered back to them to post publicly.

A ticked job card below a short row of small question bubbles, with a larger speech bubble being lifted up onto a broad public board carrying a row of stars

Skip the rebuild

You do not write this one. It arrives in your project as ordinary source you can read, change and keep, with its audit record and its caveats beside it.

  1. Download the Playground. It is free and runs on your own machine.
  2. Start a new app and tick “Ask finished customers for a public review (Google, Facebook, BBB)”.
  3. This block is written in, along with anything else you ticked.

Download the Playground See the other blocks

The mistake it removes

Two failure modes, both structural. (1) REVIEW GATING: offering the public-posting step only to customers who scored well. Google prohibits it, can remove the reviews and penalize the listing, and the FTC's 2024 rule on suppressed reviews raises the exposure. (2) ONE FLOW FOR EVERY DESTINATION: Yelp asks businesses not to solicit reviews AT ALL, so a Yelp button sitting in the same row as the Google one puts the customer in breach while showing them a screen that looks fine.

What is already handled

Each of these was checked by running the code, not by reading it.

What the audit found

Named rather than summarized. The reasoning behind each one ships inside the block, so it travels with the code instead of living on a page you have to trust.

What you still have to do

A copied file cannot own your secrets, your host or your legal obligations. This is the part the block deliberately does not claim.

What lands in your project

vanilla

  • vanilla/review-widget.js
  • vanilla/review-widget.css
  • vanilla/review-destinations.js
  • vanilla/review-builder.js
  • node/review-validate.js
  • module.json

Load order matters: review-validate.js, then review-destinations.js, then review-widget.js — each throws by name if its dependency is missing rather than failing later. ReviewWidget.mount(el, config, { shadow: true, css }) renders the interview; ReviewBuilder.mount(el, config, { onSave }) is the Modules panel. `node/review-validate.js` is listed here on purpose: it is ONE rules file shared by both halves, it physically lives in node/ so the server entry can import it as a sibling in either layout, and install flattens by basename so it lands next to the widget anyway.

node

  • node/review-request-api.js
  • node/review-retention.js
  • node/review-validate.js
  • test/cases.json

createReviewRequestApi({ config, store, onStored, trustProxy, retentionDays }) returns { submit, retention }. It validates against the SAME module.json the page was built from, so a question you removed cannot be submitted and one you added is enforced with no code change. The rate-limit key is the SOCKET PEER — X-Forwarded-For is read only when the peer is in `trustProxy`, and then only the right-most untrusted entry. review-retention.js is the 90-day expiry, the owner's export and the erase-one-person path; it needs `remove(recordIds)` on the store and says so loudly rather than deleting nothing quietly. cases.json ships so the install can be re-checked against the block's own module.json. The honeypot field's name comes from module.json (`honeypot_name`, default `honeypot`) because the widget, both validators and both endpoints all have to agree on it; the `honeypotName` option is for a front end of your own, it defaults to the configured name, and the API refuses to start if the two disagree or if the name collides with a question id.

php

  • php/review-settings.php
  • php/review.php
  • php/review-validate.php
  • php/client-ip.php
  • php/rate-limit.php
  • php/review-tools.php
  • module.json
  • test/cases.json
  • test/agree.php

The shared-hosting half — upload beside the site with module.json, edit review-config.php and nothing else. It decides the same things in the same order as the Node half, and test/cases.json is run through BOTH validators (test/agree.php is the PHP side of that) so the two cannot quietly drift: the first Node port of this block reinstated a spoofable rate-limit key that this PHP had already fixed and documented, and an agreement test is what makes that visible instead of merely present. review-settings.php is the ONE reader of review-config.php and both entry points require it first — there used to be two readers with different default sets (ph105tsk79). review-tools.php is export / erase-one-person / sweep / config and REFUSES to run over HTTP — a route handing back every stored name, email, phone and IP is the worst thing on the site if its auth is ever wrong. Requires PHP 8.0+.

python

  • python/review_refresh.py

The scheduled half. `--status` says when the connector last SUCCEEDED and exits non-zero when it has not; `--install-schedule` prints a launchd plist. It never talks to a vendor itself — the connector is injected.

What you supply

Licensed MIT. It is a starting point, not a finished product.

Get it

Download the Playground See the other blocks

Nothing here is locked. The files are yours, in your folder, under a permissive license.

Questions

Is Ask customers for a public review audited?
It was audited on 2026-09-10, and the verdict was not a clean pass.
What do I still have to do myself?
Set trustProxy to the CIDR of the proxy that REALLY sits in front of the app, and put the builder panel behind admin-kit's requireAdmin — the panel rewrites the live question set and has no auth of its own.
How do I get this code?
Download the Playground, start a new app, and tick “Ask finished customers for a public review (Google, Facebook, BBB)”. The block is written into your project as ordinary source you can read and edit.

All pre-built code blocks · Learn to build from zero · The coding guide