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.
| Task | Manual lifecycle comms | Automated retention engine |
|---|---|---|
| New signup welcome | Write per subscriber, copy old drafts | One sequence generated once, personalized each send |
| Billing dunning | Ad hoc emails after failures | Timed, multi-step emails and SMS with safe retries |
| Winback timing | Guess from gut feel | Triggered from last purchase window by plan |
| Brand voice | Inconsistent between staff | Single brand spec applies to every message |
| QA and proofing | Time-consuming per send | Preview and approve in one panel |
| Error risk | High risk of double-sends | Idempotent 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.
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