Travel · 2026 In development
Skylane
Multi-provider flight search aggregator with WebSocket streaming and circuit breakers.
Tech stack
Laravel 12 Vue 3 TypeScript Tailwind CSS v4 Reverb Horizon PostgreSQL 16 Redis Filament 3 Pest
The problem
Real flight search aggregators talk to a mix of GDS systems, airline-direct APIs, and price-index data sources. Each one returns a different schema, has different rate limits, fails in different ways, and ships full vs partial data. Typical aggregator tutorials fan out to mock APIs that all return clean data on the same clock. That hides the entire problem.
Goals
- Three real providers (Amadeus GDS, Duffel NDC-style, Travelpayouts price-index) talking to live APIs
- Canonical schema that gracefully handles full and partial provider data
- Live result streaming so the user sees offers as each provider responds, not after the slowest one finishes
- Per-provider isolation so one provider failing does not poison the others
The solution
- Queue-backed parallel dispatch via Horizon-managed jobs, one job per provider per search
- Per-provider adapter + normalizer pair that maps response to a canonical FlightOffer DTO
- Result store at the Redis boundary with a dedupe key (carrier + flight number + date + segments hash) so duplicate offers never broadcast to the UI
- Circuit breaker per provider that opens after N failures in a rolling window and recovers automatically after cooldown
- Reverb WebSocket channel scoped to the search ID so the UI subscribes once and gets streamed inserts
My role
- → Architecture and provider abstraction design
- → Each adapter built as a thin Laravel Http client (Guzzle under the hood) plus normalizer
- → Reverb + Echo wiring for live streaming
- → Filament admin with custom widgets
- → Pest test suite covering adapters, normalizers, dedupe, and circuit breaker state machine
UI direction
Vue 3 + Tailwind v4 search page subscribing to Reverb over Echo. Filament 3 admin for operator visibility into provider health.
User flows
Live search
- 1 User submits origin, destination, dates, passengers
- 2 Backend creates a search row and dispatches one Horizon job per active provider in parallel
- 3 Each job calls its provider adapter, normalizes the response, dedupes against the search's result store, and broadcasts new offers on the search's Reverb channel
- 4 Vue UI subscribes to the channel and renders offers as they arrive, partial-data offers labeled accordingly
- 5 Slow providers continue streaming until the search timeout; the user sees results from fast providers immediately
Screenshots
Click any image to open at full size.
Key learnings
- No PHP SDK exists for Amadeus, Duffel, or Travelpayouts; thin Http clients plus per-provider normalizers turn out to be the right shape, not a heavyweight SDK abstraction
- Dedupe at the result-store boundary, not in the UI; the UI cannot un-render an offer that already broadcast
- Circuit breakers are non-optional once you have three real providers; one provider going slow can cascade into queue starvation otherwise
Want something like Skylane?
I'm open to senior contract work. Let's talk about what you're building.
Get in touch