Context
This client runs a subscription-first nutrition brand. The majority of their revenue — around $19.2m annually — comes from monthly subscribers who receive a recurring shipment of their flagship product.
They had been using a leading third-party subscription platform since launch, and it had served them well in the early days. But at $19.2m in subscription revenue, the 1.25% platform fee was a $240k annual line item. And the platform’s inflexibility had become a strategic constraint: every retention experiment — custom cancel offers, skip discounts, pause flows — required raising a support ticket and waiting weeks for the platform to build it.
The CEO’s question was direct: Can we own this ourselves?
Challenge
The technical challenges of rebuilding subscriptions from scratch on Shopify are significant. This isn’t an inventory app — it touches billing, customer trust, payment vaulting, and compliance.
Challenge 1: Payment vaulting. Shopify doesn’t let custom apps charge customers on a schedule without explicit customer authorisation. The Shopify Billing API handles this correctly for platform apps, but building your own subscription system requires customers to specifically grant recurring payment permissions via the customer payment methods flow.
Challenge 2: Migration risk. Moving 8,400 active subscribers from one billing system to another is high-stakes. Any gap in billing causes charge failures. Any UX disruption causes cancellations. We had to plan a zero-downtime migration with fallback capability.
Challenge 3: Retention flow complexity. The existing platform’s cancellation flow was a simple “Are you sure? Yes/No.” The client had anecdotal evidence that subscribers cancelled for addressable reasons — price, pace of consumption, travel — but no tooling to catch them. Building meaningful retention offers required a properly structured cancel intent flow.
Challenge 4: Customer account UI. Shopify’s new Customer Accounts Extensibility (the replacement for classic customer accounts) requires building UI extensions using Shopify’s UI Extensions framework — a different paradigm from standard web development.
Action
Phase 1 — Architecture and Technical Scoping (Weeks 1–2)
We mapped every subscriber touchpoint: initial sign-up, upcoming charge notification, skip, pause, swap product, change address, cancel, reactivate. Each state needed a corresponding API endpoint and UI state.
The final architecture:
┌────────────────────────────────────────────────────────────┐
│ Custom Subscription App │
├──────────────────┬─────────────────────────────────────────┤
│ Customer UI │ Admin Dashboard │
│ (Account Ext.) │ (Polaris) │
│ │ │
│ - View plan │ - Subscriber list + search │
│ - Skip/Pause │ - Manual billing/refunds │
│ - Swap product │ - Retention offer analytics │
│ - Cancel flow │ - Churn reason reporting │
└──────────────────┴─────────────────────────────────────────┘
│ │
└──────────┬───────────────┘
▼
┌──────────────────────┐
│ Subscription Engine │
│ │
│ - Billing scheduler │
│ - Payment retry │
│ - Dunning logic │
│ - Webhook dispatch │
└──────────────────────┘
│
┌──────────┴──────────┐
│ Shopify APIs │
│ │
│ - Billing API │
│ - Orders API │
│ - Customer API │
│ - Payment Methods │
└─────────────────────┘
Phase 2 — Core Billing Engine (Weeks 3–7)
The billing engine runs on a cron job that processes due subscriptions in batches. The key complexity is handling payment failures gracefully without churning customers unnecessarily.
We implemented a four-attempt dunning sequence:
Day 0: First charge attempt → if fails, log and schedule retry
Day 3: Second attempt + email notification ("Payment failed — please update card")
Day 7: Third attempt + more urgent email
Day 14: Final attempt + "Subscription paused" notification
Day 21: Subscription cancelled if still unpaid (final win-back email)
Each step sends a Klaviyo transactional email. The dunning flow recovered 31% of initially failed payments in the first month — a meaningful churn reduction before any cancel-flow work was done.
Phase 3 — Retention Cancel Flow (Weeks 7–10)
This was the highest-value engineering investment. The cancel flow presents personalised offers based on the stated cancel reason:
| Cancel Reason | Retention Offer |
|---|---|
| Too expensive | 30% off next 3 months |
| Consuming too slowly | Free skip (resume in 30/60/90 days) |
| Travelling / not home | Pause for up to 3 months |
| Product not working | Free 1:1 nutrition consult |
| Switching to competitor | 25% off for 6 months |
| Other | Generic 20% discount |
The cancel reason and offer acceptance/decline are tracked as Shopify events and stored in the app database for cohort analysis.
// Cancel flow — offer selection logic
export function getRetentionOffer(cancelReason, subscriberMonths) {
const loyaltyBonus = subscriberMonths >= 12 ? 'loyalty_upgrade' : null;
const offers = {
too_expensive: { type: 'discount', value: 30, months: 3 },
consuming_slowly: { type: 'skip', days: [30, 60, 90] },
travelling: { type: 'pause', maxDays: 90 },
not_working: { type: 'consult', calendlyUrl: process.env.CONSULT_URL },
competitor: { type: 'discount', value: 25, months: 6 },
other: { type: 'discount', value: 20, months: 2 },
};
const offer = offers[cancelReason] || offers.other;
if (loyaltyBonus) offer.bonus = 'Free product upgrade on next order';
return offer;
}
Phase 4 — Migration (Weeks 11–14)
The migration strategy: run both systems in parallel for 30 days. New subscribers enrol in the new system immediately. Existing subscribers are migrated cohort by cohort (newest first, oldest last) over four weeks.
Migration steps for each subscriber:
- Create matching subscription record in new system
- Verify next billing date matches (to the day)
- Do NOT cancel in old system yet — just pause it
- Run first billing cycle in new system
- If charge succeeds: cancel the old system subscription
- If charge fails: fall back to old system, investigate
The fallback capability meant zero subscribers experienced a billing gap. We migrated all 8,400 in four batches across four weeks. Total failed charges during migration: 12 (all due to expired cards, not system issues). Total zero-downtime minutes: 0.
Result
$240k in annual platform fees eliminated. The app development investment paid back in under 10 months.
Subscriber churn reduced 14%. The dunning flow (recovering failed payments) accounted for roughly half of this. The cancel offer flow accounted for the rest. Of subscribers who reach the cancel flow, 34% now accept a retention offer instead of cancelling.
Engineering velocity unlocked. In the six months since launch, the team has shipped four A/B tests on the cancel flow without any vendor bottleneck. One test found that showing the “pause” option first (before the cancellation confirmation) reduced cancellation rate by an additional 8% vs presenting it only when the stated reason was “travelling.”
Churn reason data. The cancel flow now surfaces structured churn reason data that never existed before. “Too expensive” accounts for 41% of cancels — the single largest reason. The brand is now designing a lower-cost entry tier to intercept this cohort before they cancel, which wouldn’t have been possible without the data.
The economics of subscription platform fees at scale are brutal. At $10m+ ARR, 1–2% platform fees are a serious EBITDA line. But the real win here wasn’t just the cost saving — it was owning the cancel flow. That’s where subscription businesses win or lose.