The Pulse by The Good Builder
A commercial multi-client SaaS that lets builders survey their customers at every stage of a build. Satisfaction is tracked as a trend, not one end score. Detractors trigger a same-day recovery call; promoters get asked for a Google review. Sold at $2,000 per build + GST (competitors charge $5,000-6,000). Standalone service, proxied at mc.increaseconstruction.com.au/pulse.
2
Capture layers (SMS + web)
multi
Tenant / white-label
Brand: teal #107b8c, sage #7d8d74, cream #faf7f3, Poppins. First prospect: Renovair. Ships disabled until a pilot builder onboards.
What problem it solves
A single end-of-build satisfaction score tells a builder nothing they can act on. The Pulse asks a short question at each stage of the build, so a dip shows up while the job is still live and can be recovered. It turns customer experience into a running trend line with named-staff accountability, automatic detractor recovery, and a promoter-to-Google-review funnel — the loop most builders never close.
Architecture
A single Express service (port 3013, ESM, better-sqlite3 WAL) that does four things: serves the admin dashboard SPA (public/index.html); serves a public, HMAC-token-guarded mobile survey page (/survey); captures responses two ways (inbound JustCall SMS webhook + web deep-dive form); and runs three background pollers — survey sender (60s), recovery-case reminders (hourly), non-responder handoff sweep (daily). Its own database file (data/pulse.db) means no shared-file contention with Mission Control. Caddy proxies /pulse* to it; the app is path-prefix aware via PULSE_BASE_PATH.
How a survey flows
1
Build enrolled as a job
A build enters as a csat_job — either pulled from the builder's CRM (Pipedrive, idempotent on provider+ref) or CSV-imported. It carries customer contact, consent, current stage, and the named staff in each role.
2
Admin marks a stage reached
Marking a stage enqueues a csat_survey (one row per job x stage, UNIQUE). Triggers are manual for the pilot; the model supports automation later.
3
Sender poller sends the link
Every 60s the sender picks up pending surveys and sends one HMAC-signed link by SMS (JustCall), falling back to email (Google SA). The link is timing-safe and carries an opt-out token; email includes an unsubscribe footer.
4
Customer responds — two ways
Either a 1-5 pulse reply over inbound SMS (/api/csat/inbound-sms) or the white-label web deep-dive form (/api/csat/respond) capturing CSAT, NPS, CES, per-role dimension scores, and a comment.
5
Side effects fire
A detractor raises an alert email + a recovery case; a promoter at the final stage gets a Google-review CTA. Recovery cases carry assignee, SLA, JustCall recording, and an AI tone/sentiment read (AssemblyAI transcript + OpenRouter).
6
No response? Daily handoff
After the automated follow-up window (PULSE_HANDOFF_DAYS, default 5) a daily sweep hands unanswered surveys to the builder's care contact to chase by hand. The build is still billable.
The build journey (residential)
prelim Design & Proposal → deposit Contract → slab Build Start → completion Practical Completion / Handover (NPS asked here) → maintenance Warranty. A commercial journey is a second configurable template. Every stage label is per-builder configurable.
Staff scorecards
Each stage measures named staff in generic roles (Sales Consultant, Customer Liaison, Selections, Site Supervisor, Maintenance Coordinator, Office — labels per-builder configurable). Each role is rated on four dimensions: Communication, Responsiveness, Clarity, Care. Scores normalise into csat_survey_roles so results aggregate by person, stage, and dimension — a builder can see which team member drives satisfaction and where it slips.
Certification integrity (anti-gaming)
The benchmark endpoint returns a certification block whose eligibility counts distinct verified phone numbers, not raw responses, plus a duplicate-number guard. A builder cannot inflate their score by surveying the same happy customer repeatedly — the certification is only as strong as the number of real, distinct customers behind it.
Multi-tenant security model
| Control | What it does |
| scopedSlug() | Pins every builder-scoped route to the caller's token, ignoring any caller-supplied ?slug= — one builder can never read another's data. |
| Per-builder viewer token | pv_... token scopes a read-only login to ONE builder. Minted via POST /api/csat/builders/:slug/readonly-token (admin, returned once). Stripped from all client-facing SELECTs. |
| Fail-closed webhook | Inbound-SMS returns 503 if JUSTCALL_WEBHOOK_SECRET is unset — it never runs unauthenticated. |
| Rate limiting | Dependency-free per-IP limiters on /survey, /api/csat/respond, /survey/optout, and AI cost paths. |
| adminOnly mutations | All writes, recovery-case edits, and AI cost paths require the admin token; reads use scoped internalOrAuth. |
| Opt-out compliance | Phone-wide STOP handling + email unsubscribe (same signed token as the survey link) mark the customer opted-out across future sends. |
| XSS hardening | Public survey and SPA escape all interpolated values (esc()/escJs()); no color-mix() so old customer phones render correctly. |
Data model (db.js)
| Table | Holds |
| csat_builders | Per-builder config: brand theme, journey, stages, Google-review URL, alert + care-contact emails, call mode, CRM provider/config, custom question, scoped readonly_token. |
| csat_jobs | One build: customer contact, consent, current stage, named staff per role, CRM provider + ref. |
| csat_surveys | One row per job x stage (UNIQUE). Lifecycle pending → sent → responded / skipped / expired / handed_off. Carries pulse_score, csat, nps, ces, comment, deep_dive. |
| csat_survey_roles | Per-survey role scores + the four dimension scores. |
| csat_recovery | Close-the-loop call cases: assignee, SLA, outcome, recording, AI sentiment/tone. |
Integrations
- JustCall — outbound survey SMS, inbound 1-5 pulse webhook, and recovery-call recording download.
- Google Workspace SA (MANDO_WORKSPACE_SA_JSON) — email fallback + alert emails.
- AssemblyAI + OpenRouter — recovery-call transcription + AI tone/sentiment read.
- Pipedrive — CRM read (pull won/open deals to jobs) and write (push survey-result note back to the deal). Adding a CRM = adding one adapter in lib/crm-sync.js.
- Slack — ops alerts for detractors, overdue recovery, and handoffs.
Pricing & billing
Flat $2,000 per build + GST. GET /api/csat/plan?slug= returns the billable-build count and the revenue it represents (ex-GST, GST, inc-GST). Billing itself is out of scope — Pulse only surfaces the number that drives it. Non-responders are billable; billing counts every enrolled build.
Deployment status
PM2 pulse on 3013, wired into start-mando.sh with an env allowlist; Infisical holds the PULSE_* tokens. Live and proxied at mc.increaseconstruction.com.au/pulse; moves to the TGB-owned thepulse.thegoodbuilder.com.au once Aaron provides DNS. Survey SENDING ships disabled (CSAT_SURVEY_ENABLED off, CSAT_DRY_RUN on) until a pilot builder onboards — the dashboard and public survey still render for demos. Repo: increase-construction/good-builder-pulse.