01
Context
A couple submits one wedding request, and the app finds local providers for each service through timed matching rounds. Providers pay one credit to unlock the couple's contact details. The app runs in English and French, with prices in EUR for France and USD for the USA.
It is a public demo at wedding-app.oldev.site. The cities, businesses, couples and phone numbers are fictional, the demo checkout takes no real payment, and the demo data is restored every day.
02
The Challenge
- One request covers many services, and each service needs its own match, its own clock and its own outcome.
- Several providers can accept the same lead at once: only one may win, and only one credit may be spent.
- Rounds must close on time even when the automation layer is down.
- Events sent to n8n can fail in transit, so they must be retried, logged and replayable.
- Two languages and two currencies across the public site, the provider area and the back office.
03
Scope and Approach
One request, many matches
A wedding request is split into one service request per service, such as photo, catering or venue, and each one is matched on its own.
Ranking
Laravel exposes the eligible providers through a versioned Sanctum API, and an n8n workflow asks the Claude API to rank them.
Timed rounds
The top providers, 3 by default, are notified on WhatsApp through the WhatsApp Cloud API and have 120 minutes to respond. If nobody accepts, the next round starts with different providers. After 3 rounds, the request opens to every eligible provider on the marketplace.
Credits and unlock
The first provider to accept pays one credit and gets the couple's contact details. Every credit movement is written to a ledger with the resulting balance.
Provider area and back office
Livewire 4 screens for providers and administrators, roles through Spatie Permission, and a language switch that covers the public site, the provider area and the back office.
Delivery
A Docker image deployed with Coolify, with MySQL or MariaDB and Redis for cache and sessions in production.
04
Architecture flow
05
Technical Deep Dive
A round state machine
Each service request moves through new, three rounds with a matching state between them, and the open marketplace, ending in accepted, expired or deleted. A scheduled command closes timed-out rounds every 5 minutes, independently of n8n.
A credits ledger inside transactions
The credits service locks the provider's row inside a database transaction and writes each movement to the ledger with the resulting balance. Accepting a lead locks the service request too, so only one provider can win it.
Signed webhooks with queued retries
Domain events reach n8n through queued jobs, signed with HMAC-SHA256. A delivery gets up to 6 tries with exponential backoff, is logged, and can be replayed from the admin area or the command line.
Structured logging
Application events are written as JSON to a dedicated channel, with common personal fields redacted.
06
Engineering Decisions
The app keeps the clock
Rounds are closed by a scheduled Laravel command, not by the automation layer, so a stalled workflow cannot leave a request waiting forever.
AI ranks, the code decides
Claude only orders the providers Laravel has already found eligible. Rounds, credits and unlocks stay in tested application code.
A narrow API for n8n
n8n reads candidates and drives rounds through a versioned API protected by a token ability and rate limits, rather than through database access.
07
Results
- A public demo in English and French, with fictional data restored every day.
- 300 PHPUnit tests covering matching, credits, webhooks, the API, the admin area and demo mode.
- One winner per lead, enforced by row locks inside database transactions.
- Rounds close on schedule even when the automation layer is unavailable.
- Every webhook delivery is signed, retried with backoff, logged and replayable.