Problem
Payment systems don’t behave like normal request/response apps. Stripe can deliver webhooks more than once, out of order, or hours later. If you treat “success” as a frontend redirect, you eventually ship double emails, inconsistent booking states, or untraceable failures.
The goal was a production system that stays correct under retries and partial failures, while still being easy to operate day‑to‑day.
Solution
I built a webhook‑driven architecture where Stripe events are persisted, deduplicated, and processed asynchronously. Payment state transitions are guarded so late or duplicate events can’t regress a booking.
Emails and fulfillment actions only trigger on the first valid transition into a paid state which allowed them to become replay‑safe.
How it works
Tool in action
Two production views: the customer-facing booking flow (trust + conversion) and the Filament admin surface used to inspect bookings, payment state, and webhook-driven fulfillment.
Booking flow: upfront payment requirement + clear session details to build trust.
Filament admin: operational visibility into booking/payment state with safe retry paths.
Challenges & solutions
Out‑of‑order webhook delivery
Stripe can send different “paid” signals in different orders. Late events can also arrive after a session expires. Without guards, a paid booking can be incorrectly downgraded.
Both primary “paid” event types converge on the same transition. Monotonic guards prevent regressions. Side effects (emails/fulfillment) only occur on the first transition into paid.
Reminder emails sent more than once
Under real usage, I noticed I was occasionally receiving reminder emails more than once due to retries and edge cases. It wasn’t constant, but it was annoying, and it immediately undermined trust.
I started seeing duplicate reminder emails when Stripe retried events or a job replayed. I fixed it by stamping each email type the first time it sends and checking that stamp before queueing again.
Duplicate deliveries and retries
Webhooks may be delivered multiple times. The system must remain replay‑safe even if Stripe retries the same event repeatedly.
Persist every event and dedupe at the database layer. Claim-and-process uses atomic updates so only one worker applies an event. Processing becomes a safe no‑op on duplicates.
Operational support on a live system
When something fails, it needs to be diagnosable and recoverable without guessing or manual dashboard spelunking.
Built Filament admin visibility for webhook/event status plus retry paths. Releases are rollback‑friendly and failures surface clearly.
Testing & validation
I validated the system by simulating duplicate deliveries, out‑of‑order sequences, and retry behavior. Critical transitions were tested to ensure “paid” cannot regress and emails do not double‑send.
I verified this in production using live Stripe test payments and observed webhook ingestion → async processing → email dispatch end-to-end.
Results & impact
From Wix to a real system: the previous flow relied on manual follow-ups and a generic booking experience. Rebuilding the site and onboarding flow made the business feel more legitimate with better UX, reliable emails, and a payment process that customers could trust.
Automated booking + payment: clients can book and pay without manual invoicing or back‑and‑forth.
Reduced operational overhead: admin view makes payment and booking state observable and debuggable.
Production‑ready reliability: system holds well under retries, duplicates, and delayed webhook delivery.
Technical deep dive
Aura didn’t want a booking system unless clients paid upfront. That makes Stripe the source of truth, so the system has to be correct under retries, duplicates, and out‑of‑order delivery. Below are the “under the hood” details: domain flow/model, webhook pipeline, and production constraints.
Domain flow
From booking intent to paid fulfillment
Early on, I treated the frontend redirect as “success” and it burned me. Sessions expired, payments failed silently, and I ended up with bookings marked paid when Stripe never confirmed anything.
I fixed it by making Stripe the source of truth. A booking only becomes paid when a confirmed webhook event arrives, and every email or reminder is stamped so retries and duplicates safely become no-ops.
Domain model
Bookings, Stripe events, and idempotent side effects
The system has two core tables: bookings (business state) and stripe_events (append‑only audit log). Booking “payment facts” are write‑protected and only mutated inside the guarded webhook processing path. Email sends are tracked with per‑type timestamps so retries never double‑send.
Note: I locked down payment fields so only the webhook processor can write them. A late "unpaid" event was overwriting confirmed bookings early on, so I made state only move forward.
Midway through development, Aura stopped offering in-person readings and shifted to phone and recorded audio formats. This required different booking flows and business rules enforced by the frontend and booking form.
I also added admin-controlled reading types and acceptance flows so Aura could manage offerings and availability without touching code.
Webhook processing
Architecture and failure modes
Stripe can deliver events multiple times and in different orders. The pipeline is designed so every step is replay-safe.
Different Stripe events can signal “paid” (for example, a session completion or an intent success). Both converge on the same transition, and only the first transition queues emails.
Reminder spam was the forcing function here: email sends are guarded by per-type sent_at stamps, so replays become a safe no-op.
Deployment & production infrastructure
Built for real constraints
What I learned
This project taught me to trust my gut and optimize for what actually moves the business forward. Aura decided to move off Wix and was already looking at Hostinger, so I initially tried to make WordPress work because it felt like the “standard upgrade.” But WordPress wasn’t my strong suit so after a month or two, I realized I was spending more time learning the platform than improving the actual customer experience.
I made the call to start over and build it from scratch with tools I’m confident in. Even if it took longer, it let me focus on what mattered: a redesign that matched her vibe, reliable email delivery, and a booking + payment flow that feels professional. That shift rebuilt trust and gave Aura the confidence boost to present herself to new businesses more seriously.
Tech stack
RosterApps Timecard
A zero-backend UserScript that injects a payroll calculator into a legacy airline timecard system.