Hey everyone,
I wanted to share an end-to-end abandoned cart recovery workflow I recently designed and built using n8n and PostgreSQL.
Most basic cart recovery setups just blindly blast emails on a timer. I wanted to solve real-world e-commerce edge cases specifically preventing duplicate triggers, stopping emails for out-of-stock products, handling automated failovers, and killing pending reminders the moment an order is paid.
The Problem It Solves:
-
Duplicate Webhook Spams: Multiple cart events or quick retries often cause repeated records and duplicate emails.
-
Promoting Out-of-Stock Products: Standard sequences email users even if an item ran out of stock after abandonment.
-
Annoying Follow-ups Post Purchase: Reminders continuing after the customer has already completed checkout.
-
Delivery Reliability: Single-provider failure halts the recovery sequence.
How the Architecture Works:
-
Intake, Deduplication & Stock Guard: Receives abandoned cart webhooks, normalizes payload data, checks live warehouse stock, and logs incoming events using idempotent database logic to discard duplicates.
-
Background Engine & Dynamic Follow-ups: A scheduled worker claims due follow-up tasks, computes tiered dynamic discounts, and routes emails with automated failover logic.
-
Payment Reconciliation & Auto-Cancellation: Captures paid order webhooks, marks carts as recovered, and immediately cancels all pending follow-up sequences.
-
Storefront & Metrics APIs: Exposes endpoints to validate applied coupons on the fly and returns recovery analytics directly from the database.
Here is a quick walkthrough demo showcasing the full pipeline, edge-case testing, and live Supabase state transitions .
I’d love to get feedback from the community on this architecture or hear how you handle high-volume cart recovery edge cases in production!
Nice build. The auto-cancellation on payment is the part most cart recovery setups get wrong.
One thing that jumped out: a lot of the edge cases you listed are really state problems. Duplicate webhooks, reminders after payment, and emails for out-of-stock items all happen when a send is allowed from a state where it shouldn’t be. Modeling each cart as an explicit state machine makes those rules visible in one place:
Abandoned → Reminder 1 Sent → Reminder 2 Sent → Recovered / Expired / Cancelled (out of stock)
Each transition has a guard. “Send reminder” is only valid from Abandoned or Reminder 1 Sent. “Mark recovered” works from any active state and ends the sequence. Once a cart is Recovered, there’s no send transition, so no reminder can go out.
It also helps with a race condition I’d test: the payment webhook arrives after the worker has claimed a follow-up task but before the email goes out. If the send step does the transition as a conditional update (only move to Reminder 2 Sent if the state is still Reminder 1 Sent), a paid cart fails the check and the email is skipped. It won’t close the window completely, since the email itself is sent outside the database, but it narrows it a lot.
A bonus: with a transition history, your recovery metrics come almost for free, such as how many carts recover after Reminder 1 vs. Reminder 2.
I actually manage my lead follow up using Inistate, which is built around this idea: every record has defined states and allowed transitions, and each transition is logged in the record’s history.
That makes it easy to answer “why did this customer get a follow up message ?” You can do the same in Postgres with a status column and guarded updates, though.
The main thing is making the states and transitions explicit.
Curious how you’re handling that payment-during-send timing right now?
Hey, really sorry for the late reply- I got caught up with a few things over the last couple of days! Thanks a lot for taking the time to write such detailed feedback.
You made a fantastic point about the state machine approach. Treating the whole flow as clear, defined states makes everything so much cleaner and prevents those messy logic overlaps.
To answer your question about the payment-during-send timing: that exact race condition was on my mind when putting this together. Right now, before the worker actually hits the email service, it runs a quick check back on the cart in PostgreSQL. If the status flipped to paid or cancelled in that split second, the workflow simply stops and drops the email right there. Like you said, since sending the actual email happens outside the database, there’s always that tiny fraction of a second, but checking the status right at the very last moment has kept it rock solid so far.
I also really like your suggestion about logging transition history. Having that clear trail makes tracking stats and debugging so much easier. Really appreciate the input, this gives me a lot of good ideas to refine the setup even further!
1 Like
building real-time inventory checks into abandoned cart recovery workflows solves the biggest headache with static drip sequences. verifying live stock levels before triggering outbound messages prevents sending reminders for out-of-stock items.
pairing deduplication logic with automated cancellation webhooks ensures customers never get spammed after completing their purchase through alternative checkout channels.