Skip to content
All work
In productionJul 2026

Customer Status Pages

Every customer gets a live page for their tickets. A model writes the updates; five checks decide whether they publish.

Next.jsSalesforce Bulk APIGuardrailsEvalsSupabaseVercel

25/25

Eval fixtures clean

Three-lens judge panel

5/5

Adversarial cases caught

Faithfulness gate self-test

26/26

Updates improved in shadow

Read-only run against live tickets

0

Salesforce calls at page load

Context

When a FieldPulse customer has an open technical support ticket, they want to know what is happening with it. The answer used to live in a spreadsheet: customer success kept per-customer trackers by hand, updating them from Salesforce and emailing them out.

That works until it doesn’t. It is manual, it goes stale between updates, and it scales linearly with the number of customers who want visibility.

This replaces that with a live page. Every Salesforce account with a support case gets one permanent link showing its open tickets, their status, and a plain-language summary of the latest update on each.

Problem

Three constraints shaped everything.

The page is public. There is no login — the link itself is the credential. Anything rendered on it is visible to whoever holds the URL, so nothing internal can ever reach it.

The Salesforce org had almost no API headroom left. A design that called Salesforce on every page load would either get throttled or push a business-critical system past its limit.

The source text was written for internal readers. The “latest update” on a ticket is the most recent outbound email from a support rep — full of internal shorthand, dollar amounts, other people’s names, and instructions that make no sense on a read-only page. It cannot be shown raw, and it cannot simply be hidden either, because then the page tells the customer nothing.

That last one is the interesting problem. The model has to rewrite internal text into something public-safe, which means it has to be trusted with a rewrite that could invent a commitment the company never made.

Architecture

Three flows that never block each other. Page loads never touch Salesforce. — click to enlarge

The system is three independent flows:

A case is created. Salesforce fires on case creation, the service mints one permanent random token for that account, and hands the URL back for Salesforce to store. Minting is idempotent — the same account always gets the same link — and event-driven only. There is deliberately no backfill, because backfilling would mint links for every historical account at once.

Every two hours, data refreshes. A scheduled job pulls case statuses and the newest outbound email per case through the Salesforce Bulk API — submit a job, poll it, download the CSV — specifically to stay off the REST quota that was already saturated. Each update runs through the cleaner, and the results are written to Supabase.

A customer opens their link. The page renders on the server from Supabase alone. Salesforce is never in the request path, so pages stay fast, survive a Salesforce outage, and add zero load to a strained org. An unknown token renders a 404.

Security is structural rather than procedural: the database has row-level security on with no policies at all, so only the server-side service role can read anything; the browser only ever receives finished HTML. Pages are marked no-index, and the referrer policy is set to no-referrer so the token cannot leak through a click-out.

The five-stage cleaner

Turning a rep’s internal email into one or two public-safe sentences is where the risk concentrates. Every stage fails closed — if anything goes wrong, the page falls back to a safe generic line rather than publishing something unverified.

  1. Cut the quoted history

    Thread history is stripped before the model sees it, so a question from three weeks ago cannot bleed into a card labeled "latest update."

  2. Generate

    The model rewrites the update in a specified voice, with internal jargon banned, and with the rule that it must sanitize rather than suppress: an email about an $18.40 payment mismatch becomes a note about a small mismatch we asked the customer to confirm. Useful, but with the number gone.

  3. Scrub for sensitive values

    Deterministic pattern matching over the output, not the input — dollar amounts, email addresses, phone numbers.

  4. Ban impossible calls to action

    The page is read-only. Any update telling the customer to "reply here" is pointing at a reply box that does not exist, so those are rejected outright.

  5. Verify, then publish

    A second model call checks the rewritten update against the source email and confirms it claims nothing the source did not say. If this check cannot complete, the update does not publish.

Outcome

The pages are live. Customers open a link and see their own tickets, with a plain-language line on each one that a model wrote and five checks cleared.

Before any of it reached a customer, the cleaner had to earn it. It has its own evaluation harness: 25 fixtures judged on three separate lenses — faithfulness to the source, status consistency and voice, and public safety — and 25 of 25 came back clean, with none flagged for review.

The harness also tests itself. Five adversarial cases go through deliberately, including a fabricated refund commitment and an instruction to reply on a page with no reply box, and the faithfulness gate has to catch all five. It does. An evaluation suite that never fails is not measuring anything, so it has to be shown failing on purpose.

Then I ran the whole thing in shadow before proposing it as a replacement: every tracked ticket re-cleaned read-only, live and shadow output diffed side by side. 26 of 26 came out better. One of them was a live update telling a customer to reply on a page that has no reply box — which is the kind of thing you only find by looking at real output rather than fixtures.

Next case study

Payments Operations Agent

Ten minutes of manual work per payment notice, automated — without the model writing a word of it.