Rex Automaton
All posts
Marketing & Content AutomationAugust 22, 20268 min read

Hanoi Drip Coffee Retention Engine Demo: Path to Production

How we built a branded retention demo for Hanoi Drip Coffee that generates lifecycle email and SMS sequences, and the exact steps to productionize it safely.

By Jacky Lei

We shipped a fully branded retention demo for Hanoi Drip Coffee that generates on-brand email and SMS sequences for onboarding, billing recovery, and winback. It gives owners a live preview of what customers would receive and shortens the buy decision for a production rollout. This post shows exactly how it works and what to add for a safe go-live.

Retention engine definition: an automation that turns subscriber events into on-brand email and SMS sequences across onboarding, billing recovery, and winback without manual drafting.

The problem it solves

Operators write the same lifecycle messages repeatedly and still miss key moments: new subscriber welcomes, card retries, and timely winbacks. Manual drafting is slow, inconsistent, and easy to forget when the team is busy roasting, packing, and serving.

TaskManual lifecycle commsAutomated retention engine
New signup welcomeWrite per subscriber, copy old draftsOne sequence generated once, personalized each send
Billing dunningAd hoc emails after failuresTimed, multi-step emails and SMS with safe retries
Winback timingGuess from gut feelTriggered from last purchase window by plan
Brand voiceInconsistent between staffSingle brand spec applies to every message
QA and proofingTime-consuming per sendPreview and approve in one panel
Error riskHigh risk of double-sendsIdempotent keys and event guards

How the automation works

We built a Next.js 16 demo that matches Hanoi Drip's brand and roast lineup. Staff pick a plan and a lifecycle moment, then the engine returns email and SMS sequences that read like they were written in-house. The demo keeps scope tight: no live ESP or cart writes. The production plan adds provider adapters, a queue, and persistence.

  • Branded demo UI: The front end renders Hanoi Drip's logo, palette, and real roast names as selectable plans. Staff can click New signup, Card failed, or Winback to see sequences for that moment.
  • AI drafting engine: A server route calls an LLM with a strict JSON schema and brand rules. It returns subject lines, email bodies, and SMS copy that match tone and length guidelines.
  • Deterministic rendering: We template the output into HTML email and compliant SMS lengths. The model never computes prices or discounts. Deterministic JavaScript handles those.
  • Safety rails: We cap token sizes, filter profanity, and default to a canned fallback if the output violates guardrails. The demo never sends live messages.
  • Path to production: Add ESP and SMS adapters, webhook ingestion for billing events, a durable message ledger, and an approvals dashboard. Sending domains and idempotency keys keep delivery safe.

Hanoi Drip Coffee retention engine: branded UI selects Scenario, AI engine drafts sequences, you preview and approve, then production adapters send via ESP and SMS with a queue and ledger

Step-by-step: how to build it

1) Scaffold the branded demo UI

Create a Next.js route with a simple form for Plan and Lifecycle Moment. Pull brand tokens (colors, fonts, logo URL) from a config file, not hardcoded JSX.

// src/lib/brand.ts
export const brand = {
  name: "Hanoi Drip Coffee",
  palette: { espresso: "#3A2A22", caramel: "#C28A5E", crema: "#E9DFD6" },
  fonts: { heading: "Cardo, serif", body: "Inter, system-ui, sans-serif" },
  plans: ["Saigon Dark Roast", "Hanoi Espresso", "Coconut Cold Brew"],
};

Gotcha: keep all branding in one module so a rebrand never touches component code.

2) Define a strict JSON schema for outputs

The engine must return structured sequences. Enforce fields so the renderer can trust shape and length.

// src/lib/schema.ts
export type Sequence = {
  channel: "email" | "sms";
  steps: Array<{
    key: string;              // unique id like signup_day_0
    delay: string;            // ISO duration like P0D, P1D
    subject?: string;         // email only
    body: string;             // plaintext or HTML-safe string
    maxChars?: number;        // sms only
  }>;
};

Gotcha: give every step a deterministic key. You need it later for idempotency and editing.

3) Draft sequences on the server

Call your model from a server route. Pass brand tokens, plan, lifecycle moment, and the schema. Enforce max token sizes and timeouts.

// app/api/draft/route.ts
import { NextRequest, NextResponse } from "next/server";
import { brand } from "@/lib/brand";
import { z } from "zod";
 
const Req = z.object({ plan: z.string(), moment: z.enum(["signup","card_failed","winback"]) });
 
export async function POST(req: NextRequest) {
  const { plan, moment } = Req.parse(await req.json());
  const system = `You are a lifecycle copywriter for a specialty coffee brand.
Write concise, friendly copy. Never invent prices or discounts.
Tone: warm, knowledgeable, not salesy.
If asked for pricing, place a placeholder like {{PRICE}}.`;
  const user = JSON.stringify({ brand, plan, moment, schema: "Sequence" });
  const result = await draftWithLLM({ system, user, maxTokens: 1200, timeoutMs: 30000 });
  const seqs = normalizeAndValidate(result); // trims length, enforces fields
  return NextResponse.json({ ok: true, sequences: seqs });
}

Gotcha: never expose keys to the browser. Server only. Add a 30s ceiling so UX does not stall.

4) Render HTML emails and SMS safely

Use deterministic templates for layout and slot in copy. The model supplies words, not HTML structure.

// src/lib/templates.ts
export function renderEmail(step) {
  return `<!doctype html><html><body style="font-family:${"Inter, Arial, sans-serif"};color:#2B241E">
  <table width="100%" cellpadding="0" cellspacing="0" role="presentation">
    <tr><td style="padding:24px">
      <h1 style="font-family:Cardo,serif;color:#3A2A22;margin:0 0 12px">${step.subject ?? "Welcome to Hanoi Drip"}</h1>
      <p style="line-height:1.5;margin:0">${safeHtml(step.body)}</p>
    </td></tr>
  </table></body></html>`;
}
 
export function renderSMS(step) {
  const body = step.body.replaceAll(/\s+/g, " ").trim();
  return body.length <= (step.maxChars ?? 300) ? body : body.slice(0, (step.maxChars ?? 300) - 1) + "...";
}

Gotcha: never let the model return raw HTML for layout. Your renderer owns structure and sanitization.

5) Add guardrails and fallbacks

Block unsafe or off-tone content and fall back to canned messages if needed.

// src/lib/guardrails.ts
const banned = [/\bNSFW\b/i, /\$\d{1,}/, /bitcoin/i];
export function guard(step) {
  if (banned.some(rx => rx.test(step.body))) {
    return { ...step, body: "Thanks for being with us. Your account has an update. Please check your portal or reply if you need help." };
  }
  return step;
}

Gotcha: do not block legitimate currency placeholders. Use and replace downstream.

6) Prepare the production adapters

Write adapters and a queue worker, but keep them behind feature flags until domains and compliance are ready.

// src/lib/adapters.ts
export interface SendJob { key: string; channel: "email"|"sms"; to: string; subject?: string; body: string; dedupeKey: string; }
export interface Sender { send(job: SendJob): Promise<{ id: string }>; }
export class Queue { async enqueue(job: SendJob) { /* push to your queue */ } }
 
export async function scheduleSend(sender: Sender, q: Queue, job: SendJob) {
  // dedupe by dedupeKey to prevent double-sends
  if (await alreadySent(job.dedupeKey)) return { skipped: true };
  await q.enqueue(job);
  return { queued: true };
}

Gotcha: compute dedupeKey from subscriber id plus step key plus event timestamp bucket. That prevents double-sends across retries.

Where it gets complicated

Branded alias hygiene. Our demo used a clean branded alias. On every redeploy you must re-alias the same URL or update links before sharing. Broken demo links erode trust.

Auth posture. We disabled SSO for frictionless review. Before production, add authentication and role checks so only staff can preview and approve sequences.

Draft cleanup. A simple Gmail draft connector cannot delete stale drafts programmatically. Use an ESP API that supports delete and track the ESP message id per step in your ledger for idempotency.

Real billing signals. Dunning flows must fire from reliable payment events. Email bounces or support tickets are not system-of-record. In production, wire to your real billing event source and handle retries and final failure states.

Opt-in and locale rules. SMS consent, quiet hours, and opt-out language vary by country. Gate SMS sends behind explicit consent flags and store the audit trail.

Never let AI calculate money. Discounts, totals, and taxes are deterministic. Keep that math in code and require human confirmation for incentive changes.

What this actually changes

For a specialty coffee subscription, this moved the conversation from abstract copy decks to a live, branded preview that owners can react to in minutes. A production rollout replaces ad hoc messages with consistent, timed sequences that run without staff intervention.

One useful reference point: a 5 percent increase in customer retention can increase profits by 25 to 95 percent according to Harvard Business Review. Source: https://hbr.org/2014/10/the-value-of-keeping-the-right-customers

Frequently asked questions

Is this live in production or a demo?

It is a working demo we built and deployed for Hanoi Drip Coffee. It generates real sequences but does not send them. The production path adds provider adapters, a queue, persistence, approvals, and compliance checks.

Can this connect to our email and SMS tools?

Yes. In production we wire the engine to your ESP and SMS provider and ingest cart or billing events from your stack. The demo keeps that layer mocked on purpose so anyone can evaluate copy and flow safely.

How long does a production rollout take?

A focused first phase typically lands in about a week once assets are ready: brand tokens, sending domains, consent posture, and lifecycle triggers. Larger stacks and approvals can add time. We ship in increments so value appears early.

What does it cost to run monthly?

The demo itself has negligible infrastructure cost. In production, your ESP and SMS provider fees apply and the AI drafting layer adds usage-based spend. Good designs keep the AI out of the send path and only draft when copy changes, which keeps costs low.

Can a non-technical owner manage it?

Yes. We expose an approvals panel and a toggle per sequence. Owners adjust subject lines and tone and press Approve. The system handles timing, retries, and logging behind the scenes.

How do you prevent off-brand or risky messages?

We use strict schemas, deterministic templates, banned-content filters, and a human approval gate for sensitive sequences. Prices and discounts are never computed by the model and must be supplied by code or a human.

If you want this running for your store, we have already done the engineering. See our service overview at /services#workflow-automation, read how we structure billing recovery in /blog/stripe-dunning-emails-automation-guide, and book a 15-minute call. We will map your stack and sequence plan before we quote anything.

Want us to build this for you?

15-minute discovery call. No pitch. We tell you what to automate first.

Book a Discovery Call

Related reading