Laravel 12 backend for a ticket resale platform — Ticketmaster sync, organizer listings, state-machine offers, Stripe-gated quotas.
📖 Live API documentation: https://alihamzahq.github.io/pulsepass-api/
(rendered via Redoc, redeployed on every push to main)
The hard part of a ticket resale marketplace isn't the CRUD — it's what
happens the instant a seller accepts an offer. Pulsepass models that
moment as a real state machine: accepting one offer flips the listing to
sold and atomically auto-rejects every other pending bid on the same
listing, all inside a single DB transaction. Around that core sits a
nightly Ticketmaster sync with per-event transactions, Stripe
subscriptions that gate per-tier listing quotas (Free=1 / Pro=10 /
Festival=∞), and an idempotent webhook layer that survives Stripe's
retry storms.
Same shape as StubHub, Vivid Seats, or SeatGeek — pared down to the backend pieces that demonstrate senior-tier engineering.
Three files capture the most interesting decisions:
app/Domain/Offers/Actions/AcceptOffer.php— the cascade that atomically transitions an offer, flips the listing, and rejects siblings.app/Domain/Offers/States/OfferState.php— the offer state machine, declared once.app/Http/Controllers/Billing/WebhookController.php— Stripe webhook handling with event-id deduplication.
- Modular monolith — five business domains live under
app/Domain/, thin HTTP adapters underapp/Http/{Controllers,Requests}/<Domain>/. - State machine for offers — five states (
Sent→Accepted | Rejected | Withdrawn | Expired) with guarded transitions viaspatie/laravel-model-states. Accepting an offer flips the listing tosoldand auto-rejects all sibling offers in one transaction. - Idempotent Stripe webhooks — every webhook event ID is recorded in a dedup table; replays are no-ops.
- Per-event transactions in the Ticketmaster sync — one bad event cannot abort the whole batch; partial-success is the design goal.
- Tiered listing quotas via Stripe Cashier — Free (1 active listing),
Pro (10), Festival (unlimited). Quota is enforced at
POST /listingsby reading the user's current subscription. - 27 documented endpoints — auto-generated OpenAPI 3.1 spec via
dedoc/scramble, published to GitHub Pages with Redoc on every push tomain. - 113 Pest tests, PHPStan level 8, Laravel Pint — enforced in CI.
- k6 load test for the offer-create hot path — illustrative
single-machine run holds
p95 < 500msat 50 concurrent VUs (seescripts/loadtest/). - Laravel Telescope in dev only — gated to
APP_ENV=localvia the framework's official local-only install pattern; never loads in CI or production.
flowchart LR
client[Mobile / Web Client]
api[Pulsepass API<br/>Laravel 12 + PHP 8.4]
mysql[(MySQL 8)]
redis[(Redis<br/>cache + queue)]
stripe[Stripe]
tm[Ticketmaster<br/>Discovery API]
client -->|HTTPS<br/>Bearer token| api
api --> mysql
api --> redis
api -->|Cashier SDK| stripe
stripe -->|webhooks| api
api -->|nightly sync<br/>rate-limited| tm
Full data model and offer state machine diagrams live in
docs/architecture.md. Architectural decisions
are recorded in docs/adr/ — five short ADRs covering the
modular monolith layout, the choice of state-machine library, per-event
sync transactions, Sanctum over JWT, and plain Action classes.
| Layer | Choice |
|---|---|
| Framework | Laravel 12 |
| Language | PHP 8.4 |
| Database | MySQL 8 |
| Cache & queue | Redis |
| Auth | Laravel Sanctum (personal access tokens) |
| Payments | Laravel Cashier (Stripe) |
| State machine | spatie/laravel-model-states |
| OpenAPI generation | dedoc/scramble |
| Test framework | Pest |
| Static analysis | PHPStan level 8 |
| Code style | Laravel Pint |
| Local orchestration | Docker Compose |
| CI | GitHub Actions |
| Load testing | k6 |
Requirements: Docker and Docker Compose. Nothing else needed on the host.
git clone git@github.com:alihamzahq/pulsepass-api.git
cd pulsepass-api
cp .env.example .env
docker compose up -d
docker compose exec app php artisan key:generate
docker compose exec app php artisan migrate --seedThe API will be reachable at http://localhost:8080. Health check:
curl http://localhost:8080/api/health# Register and capture a Sanctum token
TOKEN=$(curl -s -X POST http://localhost:8080/api/auth/register \
-H 'Accept: application/json' \
-d 'name=Demo&email=demo@example.com&password=password123&password_confirmation=password123' \
| jq -r .token)
# Use the token on any authenticated endpoint
curl http://localhost:8080/api/offers \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Accept: application/json'docker compose exec app composer test # Pest, 113 tests
docker compose exec app composer lint # Pint --test + PHPStan level 8
docker compose exec app composer fmt # Pint, in-place fixThree commands run on the scheduler container:
| Schedule | Command | Purpose |
|---|---|---|
| Daily 03:00 | events:sync |
Pull a batch of Ticketmaster events |
| Every 5 min | offers:expire |
Transition past-due Sent offers to Expired |
| Daily 04:00 | subscriptions:cancel-past-due |
Cancel subscriptions stuck in past_due beyond a threshold |
The OpenAPI 3.1 spec is generated automatically from controllers and
form requests via dedoc/scramble. Locally:
- JSON spec:
http://localhost:8080/docs/api.json - Redoc viewer:
http://localhost:8080/docs/api
A GitHub Action publishes the spec to GitHub Pages on every push to
main. Once the repo is public, the live docs site is the canonical
reference.
app/
├── Domain/ # business core, no HTTP awareness
│ ├── Auth/Actions/ # RegisterUser, LoginUser, LogoutUser
│ ├── Billing/Models/ # Plan, WebhookEvent
│ ├── Events/ # Ticketmaster sync — Models, Jobs, Clients
│ ├── Listings/ # Listing model + status enum
│ ├── Offers/ # Offer model, 5 Actions, 5 State classes
│ └── Organizer/Models/ # Organizer profile
├── Http/
│ ├── Controllers/<Domain>/ # thin HTTP adapters per domain
│ ├── Requests/<Domain>/ # Form Requests per domain
│ └── Resources/ # API Resources (flat)
├── Console/Commands/ # 3 scheduled commands
├── Models/User.php # Cashier `Billable` lives here
└── Policies/ # ListingPolicy, OfferPolicy
docs/
├── adr/ # 5 architecture decision records
└── architecture.md # 3 Mermaid diagrams
scripts/
└── loadtest/ # k6 script + seed command + docs
A k6 load test for POST /offers is included. Setup, run instructions,
and sample results are in scripts/loadtest/README.md.
MIT — see LICENSE.