I Built a System to Stop Expiring Cards From Costing MRR
Overview
Credit card expiry is one of the simplest causes of involuntary churn to overlook. A customer can be completely satisfied with a SaaS product, have no intention of cancelling, and still disappear as a paying customer because the payment method attached to the subscription reaches its expiration date before the next renewal can be collected.
This case study describes a preventive billing workflow that identifies expiring payment methods before they become a failed payment, communicates with affected customers through Klaviyo, and automatically stops the communication as soon as the underlying payment problem is resolved. The architecture combines Stripe Billing, Supabase/Postgres, Klaviyo, Vercel, and GitHub, with each system given a clearly defined responsibility.
The objective is straightforward: move payment recovery from a reactive process that begins after revenue has failed to a preventive process that identifies a known billing risk while there is still time to resolve it.
The Problem: Churn Can Happen Without a Cancellation
Most SaaS companies pay close attention to voluntary cancellations because they produce an obvious event: the customer clicks Cancel, the subscription changes state, and the business can record why the customer left. Involuntary churn is harder to see because the customer may never make a decision to leave. Their card expires, the next recurring payment fails, and the subscription eventually enters a recovery or cancellation state.
That distinction matters because the underlying customer relationship may still be healthy. The customer may still want the product, may have no complaint about the service, and may not even know that their payment method is about to become invalid. From the business's perspective, however, a perfectly viable subscription has become a revenue-recovery problem.
A useful way to think about card expiry is therefore as an observable risk rather than an unexpected failure. Stripe already has the payment-method expiry information, which means the business can identify the risk before the invoice is attempted. The question is whether that information is connected to an appropriate customer communication and recovery process.
The Approach
The architecture is deliberately simple: Stripe remains the source of truth for payment information, Supabase maintains the application's monitoring state, Klaviyo handles lifecycle communication, and Vercel provides the application and API layer. A scheduled process identifies payment methods approaching expiry, writes the relevant state to Postgres, synchronizes customer properties to Klaviyo, and allows a date-based flow to manage the customer communication.
The important design principle is that the email platform should not become the billing system. Klaviyo is told that a customer is at risk; Stripe remains responsible for determining whether the payment method is actually valid and which payment method is relevant to the subscription.
1. Identify Expiring Payment Methods
A scheduled Supabase Edge Function periodically checks payment methods associated with active customers and records the information required for monitoring. The application needs the Stripe customer ID, payment method ID, card brand, last four digits, expiry month and year, monitoring status, and timestamps that allow the team to understand when the record was created or last synchronized.
The monitoring state belongs in Postgres rather than in Klaviyo because the database needs to preserve the history of what happened. A customer can move from watching to resolved, cancelled, or expired, and those transitions are useful for reporting and troubleshooting long after the email flow has finished.
Database example
create table card_expiry_watchlist (
id uuid primary key default gen_random_uuid(),
stripe_customer_id text not null,
stripe_payment_method_id text not null,
card_brand text,
card_last4 text,
exp_month integer not null,
exp_year integer not null,
status text not null default 'watching',
klaviyo_synced_at timestamptz,
updated_at timestamptz default now(),
created_at timestamptz default now(),
unique (stripe_customer_id, stripe_payment_method_id)
);2. Calculate the Expiry Window
The scheduled function calculates how close each relevant payment method is to expiry. I would use a 45-day monitoring window so that the system has enough time to identify the risk, synchronize the customer record, and give the customer several opportunities to resolve the problem without creating unnecessary communication for cards that are months away from expiry.
The monitoring window and the communication schedule should remain separate. A customer might enter the database at 45 days, receive the first message at 30 days, receive a reminder at 14 days, and receive a final notification at three days. If the customer updates the payment method at any point, the monitoring record is resolved and the remaining communication should stop.
Example expiry calculation
function daysUntilExpiry(expMonth: number, expYear: number) {
const expiryDate = new Date(expYear, expMonth, 0);
const today = new Date();
const difference =
expiryDate.getTime() - today.getTime();
return Math.ceil(
difference / (1000 * 60 * 60 * 24)
);
}
const WATCH_WINDOW_DAYS = 45;
if (daysUntilExpiry(expMonth, expYear) <= WATCH_WINDOW_DAYS) {
// Add or update the customer in the watchlist
}3. Sync the Risk State to Klaviyo
Once a customer enters the watchlist, the application synchronizes a small set of billing-related properties to a Klaviyo profile. These properties give the communication layer enough context to personalize the message without requiring Klaviyo to query Stripe directly.
Typical properties might include the card expiry date, last four digits, card brand, a secure link to the customer's billing page, and a status property such as card_expiry_status = at_risk. The status is particularly important because it provides a clean eligibility signal for the flow and gives the application an equally clear way to remove the customer from the sequence once the risk has been resolved.
Example Klaviyo profile properties
{
"card_exp_date": "2026-10-31",
"card_last4": "4242",
"card_brand": "visa",
"card_expiry_status": "at_risk",
"card_update_url": "https://app.example.com/settings/billing"
}4. The Klaviyo Flow
The customer can then enter a date-based Klaviyo flow that uses the expiry information to control the timing of the communication. The sequence should be deliberately short because the objective is to help the customer complete a billing action, not to create another marketing campaign.
At approximately 30 days before expiry, the message should be informational and low pressure: the customer's payment method is approaching its expiration date and they can update it through the billing page. At 14 days, the message becomes a straightforward reminder that the payment method still needs attention. At three days, the message makes the consequence clear: the customer should update the payment method before the next renewal to avoid a failed payment or interruption to service.
The tone should remain practical throughout. These customers did not necessarily do anything wrong, so the communication should read as a useful warning rather than a collection notice.
Why I Would Keep This Separate From Generic Marketing
A card-expiry notification is fundamentally different from a promotional campaign because it is tied to the customer's billing relationship. It needs accurate customer state, a reliable exit condition, a direct billing action, and safeguards against duplicate communication.
For that reason, I would keep the payment-expiry flow separate from ordinary marketing campaigns, using the same discipline I write about in how to build a Stripe dunning engine that actually recovers revenue. The customer should not receive a billing-risk sequence simply because they happen to be subscribed to a newsletter or another promotional segment. The eligibility decision should come from the billing application, while Klaviyo remains responsible for delivering the appropriate lifecycle communication.
5. Stop the Flow When the Customer Updates Their Card
The most important part of the workflow is not sending the first reminder; it is knowing when to stop. If a customer receives the first message and immediately replaces the expiring card, sending another reminder two weeks later creates a poor customer experience and makes the automation appear disconnected from the billing system.
Stripe's webhook event stream provides what's needed to close this loop. When the relevant payment method changes, the application receives the appropriate webhook, identifies the customer and payment method, updates the monitoring record, and changes the customer's Klaviyo state from at_risk to resolved. The communication flow can then use that state as its exit condition.
The result is a system that responds to the current billing state rather than blindly following a preconfigured sequence.
Example Stripe webhook handler
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text();
const signature = req.headers.get("stripe-signature");
if (!signature) {
return new Response("Missing signature", { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch {
return new Response("Invalid signature", { status: 400 });
}
switch (event.type) {
case "payment_method.attached": {
const paymentMethod =
event.data.object as Stripe.PaymentMethod;
const customerId =
typeof paymentMethod.customer === "string"
? paymentMethod.customer
: paymentMethod.customer?.id;
if (customerId) {
await markCardExpiryResolved(
customerId,
paymentMethod.id
);
}
break;
}
}
return Response.json({ received: true });
}Webhook Idempotency
A production payment workflow also needs to assume that webhook events can be delivered more than once. Stripe retries webhook deliveries when the receiving endpoint does not acknowledge them successfully, so processing the same event twice must not create duplicate database transitions or trigger repeated downstream actions. I go through the full list of events worth tracking for this reason in the 12 Stripe webhook events that decide whether your SaaS gets paid.
A dedicated Stripe events table provides a straightforward starting point for idempotency. The event ID becomes the unique key, allowing the application to determine whether an event has already been processed before performing the associated state transition.
Example event table
create table stripe_events (
event_id text primary key,
event_type text not null,
processed_at timestamptz default now()
);Choosing the Correct Payment Method
Customers can have more than one payment method, which introduces an important edge case. If a customer has a Visa expiring next month and a Mastercard that remains valid for another two years, the system should not automatically send an expiry notification for the Visa if Stripe is configured to charge the Mastercard.
The monitoring process therefore needs a clear rule for determining which payment method is relevant to the subscription. A sensible hierarchy is to check the subscription-level default payment method first, followed by the applicable customer or invoice default, and only then fall back to another explicitly defined active payment method. The exact hierarchy should follow the SaaS's Stripe Billing configuration.
This distinction is important because the existence of an expiring card does not necessarily mean that the customer's subscription is at risk. The system is interested in the payment method that will actually be used for collection.
Assumptions About the SaaS
For this implementation, I am assuming a typical subscription SaaS that uses Stripe Billing for recurring monthly or annual subscriptions and automatic payment collection. Each customer has a stable Stripe Customer ID associated with an application user, and the SaaS provides a secure billing page or Stripe-hosted billing experience where customers can update their payment method.
The application does not store raw card numbers, security codes, or other sensitive card credentials. Stripe handles the payment details, while the application's database stores references such as the Stripe customer ID, payment method ID, card brand, last four digits, and expiry information required for monitoring.
The system also assumes that Klaviyo profiles can be associated with the application's customer identity through a stable identifier. This is preferable to relying exclusively on an email address because customers can change their email address while remaining the same Stripe customer.
Why Supabase?
Supabase provides PostgreSQL as the system of record for the application's monitoring state. That gives the workflow a relational model rather than forcing the email platform to act as a database.
The distinction becomes useful when the business wants to answer questions about what happened to an at-risk customer. The database can record when the customer entered the monitoring window, whether the profile was synchronized successfully, when the payment method was updated, and whether the subscription was subsequently renewed.
That history also creates a foundation for reporting. For example, the team can compare customers who entered the watchlist with customers whose payment methods were updated before expiry, then connect those outcomes to recurring revenue.
Example reporting query
select
status,
count(*) as customers
from card_expiry_watchlist
group by status;Why Klaviyo?
Klaviyo handles the communication layer, which means the application does not need to build its own email delivery infrastructure, scheduling system, templates, suppression logic, or engagement reporting. The application determines the customer's billing state and passes the relevant properties to Klaviyo; Klaviyo then manages the customer-facing communication.
This division of responsibility keeps the architecture easier to reason about. The application answers the question, "Is this customer currently at risk?" Klaviyo answers the question, "What communication should this customer receive while that state remains true?" Stripe remains the authority for whether the payment method itself is valid.
Why Vercel and GitHub?
Vercel provides a straightforward deployment path for the Next.js application and its API layer, while GitHub provides source control and a clean deployment workflow. A typical development path is simple: code is committed to the repository, changes are pushed, Vercel builds the application, and the updated version is deployed.
This is particularly useful for a billing project because the application may eventually include a customer billing page, an internal monitoring dashboard, API endpoints, webhook handlers, and administrative reporting. Keeping those components in a conventional application stack makes the system easier to maintain as the business requirements expand.
Reliability Considerations
Payment automation needs stronger safeguards than an ordinary marketing workflow because an error can affect a customer's ability to pay for a service. The system therefore needs to account for duplicate webhook events, customers who update their payment method outside the email sequence, automatic card updates, subscription cancellations, and changes to which payment method Stripe considers the default.
The underlying rule should be that Stripe's current state takes precedence over assumptions made when the customer first entered the watchlist. If the subscription has been cancelled, the customer should no longer receive an expiry sequence. If the relevant payment method has changed, the old risk record should be resolved. If the payment method has been automatically updated, the system should recognize that the billing risk has disappeared without requiring the customer to click the email.
Measuring the System
The most useful metrics are financial and operational rather than purely email-based. Open rates and click-through rates can help diagnose the communication, but the business ultimately cares about whether fewer customers reach payment failure and how much recurring revenue remains protected as a result.
The first metric is the number of active customers entering the expiry monitoring window. The second is the pre-expiry recovery rate: the proportion of monitored customers whose relevant payment method is resolved before the expiry date. The third is the recurring revenue associated with those recovered customers. Support volume and time to resolution provide additional context about whether the workflow is reducing friction for both customers and the support team.
A Practical Measurement Framework
A useful reporting funnel is:
Customers monitored
↓
Customers notified
↓
Customers resolved
↓
Renewals successfully collected
↓
Revenue retainedFor an initial implementation, I would treat a 40-70% pre-expiry recovery rate as a working hypothesis rather than a promised result. Actual performance will depend on the SaaS's customer base, payment mix, geography, card types, billing configuration, existing Stripe recovery mechanisms, and the quality of the customer communication.
The purpose of the first production period should therefore be measurement. Once the business has enough data, the team can determine how many at-risk cards would have failed, how many were automatically updated, how many customers acted after each email, and how much recurring revenue was ultimately retained.
The Bigger Picture
The interesting part of this system is not the email sequence itself; it is the change in when the business chooses to act. Traditional dunning begins after the payment has failed, which means the customer has already experienced a billing problem. A preventive workflow starts while the payment method is still valid and gives the customer an opportunity to correct the problem before the renewal is attempted.
Traditional model:
Payment fails
↓
Customer is notified
↓
Customer fixes payment method
↓
Payment recovery beginsPreventive model:
Payment method approaches expiry
↓
Customer is notified
↓
Payment method is updated
↓
Renewal proceeds normallyThe second model does not eliminate the need for conventional dunning. Some payments will still fail for reasons that cannot be predicted from card expiry data, including insufficient funds, issuer declines, authentication requirements, and other payment failures. What it does is remove one predictable category of payment failure from the recovery queue before it becomes a problem.
For a look at what that conventional, post-failure side of the workflow looks like once it's running, Revivo is a live Stripe + Klaviyo recovery dashboard demo that shows the reactive half of the picture: soft and hard declines segmented as they arrive, an automated follow-up sequence triggered off the webhook feed, and recovery-rate reporting. It's a useful companion to this pipeline, since a healthy billing system needs both a preventive layer that catches expiring cards before they fail and a recovery layer that handles the failures that happen anyway.
Conclusion
Involuntary churn is often described as a dunning problem, but card expiry shows why it can also be treated as a data and timing problem. Stripe already knows when a payment method is approaching expiry. Supabase can maintain the state required to monitor that risk. Klaviyo can communicate with the customer. Webhooks can close the loop when the underlying payment state changes.
The resulting system is not particularly complicated, but it has a clear division of responsibility: Stripe provides payment truth, Supabase maintains durable application state, Klaviyo manages lifecycle communication, and the application connects those systems into a single workflow.
The key principle is simple: do not wait for recurring revenue to fail before responding to a billing problem that was visible in advance. A well-designed payment recovery system should not only recover failed payments; wherever the data allows it, it should prevent those failures from happening in the first place.
If you're weighing this kind of preventive billing work for your own SaaS, more build write-ups like this one are on the portfolio page, and the pricing page has the scope and cost for a Stripe audit or a dunning engine build.
Hi, I'm Mathew — I build Stripe billing infrastructure, dunning systems, and payment integrations that keep SaaS revenue from leaking. If this kind of thing is useful to you, I write more of it over on Medium and post updates on LinkedIn.
I help SaaS founders recover up to 50-60% of their “failed payment” churn through technical Stripe & Klaviyo optimization.