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.

Stripe as source of truth DB dedupe on event id Atomic claims Stamped email sends
Key outcomes
Processes real payments in production
Idempotent webhook ingestion + processing
Safe payment state transitions (no downgrades)
Bugs fixed post‑launch under real usage

How it works

Event pipeline
Verify Stripe signature and persist the raw event payload
Deduplicate using a unique constraint on stripe_event_id
Atomically claim events for async processing (no double workers)
Apply monotonic state transitions and stamp side‑effects
Invariants
Monotonic payment state: late events cannot downgrade paid bookings
Idempotent processing: duplicates become safe no-ops
Side effect isolation: emails and reminders fire once, replays safely skip

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

Challenge

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.

Solution

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.

Bug found

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.

Fix

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.

Challenge

Duplicate deliveries and retries

Webhooks may be delivered multiple times. The system must remain replay‑safe even if Stripe retries the same event repeatedly.

Solution

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.

Challenge

Operational support on a live system

When something fails, it needs to be diagnosable and recoverable without guessing or manual dashboard spelunking.

Solution

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

Live
Processing real payments
Replay‑safe
Duplicate events are no‑ops
Operable
Visibility + retries
Shipped
Bugs fixed post‑launch
Real user 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.

Core flow
Client creates a booking and is redirected to Stripe Checkout.
Stripe emits one or more webhook events (may be duplicated or out of order).
Each event is persisted in the stripe_events table (append-only, deduped).
An async worker atomically claims the event and applies guarded mutations.
On the first valid paid transition, email side effects are stamped and dispatched once.
Invariants enforced
Stripe as source of truth: frontend success alone never marks a booking as paid.
Monotonic lifecycle: booking state only moves forward (no regressions).
Idempotent side effects: emails and reminders are safe under retries.
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.

Bookings table (source of truth)
booking_status Pending • Paid • Expired • Cancelled
payment_status Unpaid • Paid • Failed
paid_at timestamp
amount_paid decimal
payment_token immutable, stable ID
stripe_checkout_id session ID
stripe_payment_intent_id PI ID
Email idempotency stamps
payment_link_sent_at
client_confirmation_sent_at
aura_notification_sent_at
Stripe events table (immutable audit log)
stripe_event_id unique index (DB dedupe)
status RECEIVED • PROCESSING • PROCESSED • FAILED
event_type checkout.session.completed, etc
payload raw JSON from Stripe
Processing state machine
RECEIVED
↓ (atomic claim)
PROCESSING
PROCESSED / FAILED / IGNORED

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.

Pipeline
Verify signature, persist raw payload, and enqueue async processing.
Atomically claim an event for processing so only one worker applies it.
Apply monotonic booking transitions and stamp email side effects.
Out-of-order handling

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.

Idempotent side effects

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
Deployment approach
Release directories + symlink switch for fast rollbacks.
Keep last N releases so rollback is a single pointer change.
No Node on server → build assets locally and deploy compiled output.
Ops & reliability
Cron-driven workers: schedule:run every minute with locks to prevent overlap.
Health checks: /up plus a custom command that checks DB/queue/Stripe.
Admin visibility for failed jobs + stuck events with explicit retry paths.

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.

Choosing the right tools beats forcing a “standard” solution.
Idempotent side effects (emails) are part of product trust.
A tailored UX can be the difference between “a site” and a real business presence.

Tech stack

Core technologies
Laravel + PHP
Webhook ingestion, jobs, and guarded state transitions
Stripe Checkout + Webhooks
Signature verification, event persistence, replay safety
Filament admin tooling
Booking visibility, Stripe event inspection, and retry workflows built with Filament.
Key practices
Monotonic state guards
Prevents regressions from late or conflicting events
Idempotent side effects
Stamped email sends and replay-safe jobs
Operational support
Retries, diagnostics, and rollback-friendly releases
Up next

RosterApps Timecard

A zero-backend UserScript that injects a payroll calculator into a legacy airline timecard system.