Openfront Airline
Airline retailing, bookings, ticketing, airport passenger service, baggage, and disruption recovery for one carrier.
Source · Live site · Catalog
Openfront Airline is an open-source, self-hostable airline retailing and passenger-service app. Travelers search flights, hold a fare, book, pay, pick seats, and check in on the public storefront. Carrier staff build the network, price and sell seats, settle payments, issue tickets, run departures and baggage, and handle disruptions in the dashboard.
It is built on Next.js 16, React 19, Keystone 6, Prisma, and PostgreSQL. To install it, see Getting started.
Tenancy and access
Carrier is the tenant. Almost every record has a direct carrier relationship, and users see only their own carrier's data. Airport is shared reference data used by all carriers.
Access follows three patterns, defined in features/keystone/access.ts:
- Carrier scoped. Normal Keystone create, read, update for the user's carrier, gated by a read and a manage permission.
- Command only. GraphQL can read the record, but every write goes through a custom command. Commands are idempotent, audited, and run in a serializable transaction.
- Station scoped. Command only, plus an active
StationAssignmentfor the flight's origin airport with the matching flag:canCheckIn,canAcceptBags, orcanBoard.
A Role holds about 30 permission flags. They cover network, inventory, offers, bookings, passengers, secure passenger data, ticketing, payments, airport service, disruptions, integrations, audit, onboarding, users, roles, and agents. A user can only grant permissions they hold themselves. canManageCarriers is the cross-carrier flag.
Data model
The model registry in features/keystone/models/index.ts registers 59 lists.
| Area | Lists |
|---|---|
| Identity and access | User, Role, Carrier, ApprovedAgent, StationAssignment, MachineCredential, OnboardingStep |
| Network and schedule | Airport, Route, FlightSchedule, FlightInstance, SeatMap, Seat |
| Product and pricing | Cabin, FareBrand, FareRule, AncillaryProduct, InventoryBucket, InventoryHold |
| Retailing and orders | Offer, OfferItem, Passenger, Booking, BookingPassenger, Journey, BookingSegment, BookingLineItem, ServiceRequest |
| Ticketing | Ticket, TicketCoupon, ServiceObligation, TicketCouponLineage, ElectronicMiscDocument, ElectronicMiscCoupon, Exchange |
| Payments | PaymentProvider, PaymentSession, Payment, ManualPaymentEvidence, Refund, SettlementLedgerEntry |
| Airport service | TravelDocument, DocumentVerification, SeatAssignment, CheckIn, BoardingPass, Bag, BagEvent |
| Irregular operations | Disruption, Reaccommodation |
| Integrations and system | IntegrationProvider, ProviderConnection, WebhookEndpoint, WebhookEvent, OutboxEvent, AuditEvent, IdempotencyKey, RateLimitBucket, WorkerHeartbeat |
Money is stored as integer minor units with an explicit currency. Offers, booking lines, segments, and tickets keep snapshots of the commercial facts at the time they were created. A Booking has separate status, paymentStatus, and ticketingStatus fields.
Main lifecycles:
- Dated flight: scheduled, open for sale, closed for sale, delayed, departed, arrived, or cancelled.
- Offer: held for 15 minutes, then accepted, expired, or withdrawn.
- Booking: pending, confirmed, ticketed, in progress, completed, or cancelled.
- Segment and coupon: confirmed, checked in, boarded, flown. Segments can also be offloaded, disrupted, reaccommodated, cancelled, or refunded.
- Bag: accepted, loaded, in transit, arrived, delivered, closed. Also delayed or mishandled.
- Disruption: detected, published, recovering, resolved, or cancelled.
Workflows
Launch. The first user signs up at /dashboard/init, which creates a carrier and a full-permission role. Then onboarding sets up the carrier, a route, a dated flight, station access, and the payment boundary.
Build the network. Register airports, create routes, author and publish flight schedules, then provision dated flights. Provisioning (provisionDatedFlight) creates the FlightInstance, its InventoryBucket, and a generated SeatMap with seats.
Price and sell. Set up cabins, fare brands (with included baggage), fare rules, and ancillaries. Set bucket authorization and open the flight for sale. An offer holds seats for 15 minutes. Accepting it creates a booking with a record locator inside the same transaction that consumes inventory. Parties are 1 to 9 travelers.
Settle and ticket. Two paths exist:
- Manual settlement (maker-checker). An operator records money received (
recordReceivedPayment). A different user approves or rejects it. An approved record is consumed bycaptureBookingPayment("Mark paid"). An outside ledger can also post evidence through the settlement-import API using a machine credential. - Stripe Checkout. If the carrier has an active Stripe payment provider, travelers see "Pay now" on Manage booking. The return from Stripe is checked against the Stripe API, the payment is recorded, and tickets are issued.
issueBookingTickets issues one ticket per passenger with one coupon per segment.
Day of operation. Operators set gates and estimated or actual times on the flight operations board. At the departure station, staff register and verify travel documents, check passengers in, accept bags, board, and offload. Bag scans after acceptance are recorded with recordBagEvent.
Disruption. publishFlightDisruption records a delay, cancellation, or schedule change with a public summary that appears on flight status. reaccommodateBooking moves a booking to a replacement flight and bucket.
Refunds and cancellation. cancelBooking (operator) or cancelCustomerBooking (traveler) cancels and releases seats. processBookingRefund records a refund against a captured payment, with approved refund evidence.
Background worker. npm run worker runs scripts/runtime-worker.ts. It expires inventory holds, expires unpaid storefront bookings after their 24-hour payment window, prunes rate-limit buckets, delivers outbox events to webhook endpoints, and writes a heartbeat that /api/health checks.
Named GraphQL operations
Custom operations live in features/keystone/mutations/index.ts.
| Group | Queries | Mutations |
|---|---|---|
| Storefront (public) | publicCarrierIdentity, publicAirlineFlightOptions, publicFlightStatuses, customerBooking | createCustomerOffer, createCustomerBooking, startCustomerCheckout, completeCustomerCheckout, assignCustomerSeat, checkInCustomerBooking, cancelCustomerBooking, rotateCustomerBookingAccessToken, recoverCustomerBookingAccess |
| Setup | machineCredentialSummaries | completeCarrierOnboarding, dismissCarrierOnboarding, updateCarrierStorefrontSettings, issueMachineCredential, revokeMachineCredential |
| Network and inventory | airlineFlightOptions | registerAirport, provisionDatedFlight, setDatedFlightSalesStatus, setInventoryAuthorization, updateFlightOperations |
| Offers and bookings | operatorBookingReceipt | createOfferFromShopping, createBookingFromOffer, cancelBooking, reissueBookingAccessCode |
| Payments and ticketing | recordReceivedPayment, submitManualPaymentEvidence, approveManualPaymentEvidence, rejectManualPaymentEvidence, captureBookingPayment, issueBookingTickets, processBookingRefund | |
| Airport service | departureManifest | registerPassengerTravelDocument, verifyPassengerTravelDocument, checkInBookingPassenger, boardBookingPassenger, offloadBookingPassenger, acceptCheckedBag, recordBagEvent |
| Recovery and system | publishFlightDisruption, reaccommodateBooking, requeueDeadLetteredOutboxEvent |
redirectToInit is also exposed for the first-run redirect. Lists without a command (routes, schedules, cabins, fares, ancillaries, station assignments, payment providers) are written with normal Keystone create and update mutations, subject to carrier access.
Integrations
- Payments. Two adapters are installed in
features/integrations/payment:manual(evidence only, needs independent approval) andstripe(Checkout sessions over the Stripe REST API). The Stripe secret key is entered on the Payment providers page and stored in write-only credentials. No environment variable holds it. - Stripe webhooks.
POST /api/payments/webhooks/[providerKey]verifies a signature, stores the event, and the worker appliescheckout.session.completedevents. The secret comes from an env variable namedAIRLINE_WEBHOOK_SECRET_<NAME>set in the provider'swebhookSecretEnvconfiguration. Set it in the environment alongside the provider. - Settlement import.
POST /api/integrations/settlements/[connectionKey]accepts settlement evidence from an outside ledger. It needs a machine credential with audiencesettlement-importand scopesettlements:write. - Outbound webhooks. The worker posts outbox events to active
WebhookEndpointrecords that subscribe to the topic. Hosts must be listed inOUTBOUND_WEBHOOK_HOSTS. - Email. SMTP sends password resets and booking access recovery. Without
SMTP_HOSTandSMTP_FROM, access recovery is turned off on the storefront. - AI assistant and MCP. The dashboard chat uses OpenRouter (
OPENROUTER_API_KEY,OPENROUTER_MODEL)./api/mcp-transportoffers generic tools (listModels,queryData,searchModels,getFieldsForType,lookupInputType,createData,updateData,deleteData,modelSpecificSearch) that run as the signed-in user. In production it returns 404 unlessAI_ASSISTANT_ENABLED=true.
Where to go next
- Getting started: install, configure, migrate, first user, onboarding.
- Deployment: build, migrate, worker, production checks.
- Carrier operations: the dashboard home and every desk.
- How-to guides: launch a carrier, take a booking, run a departure.
- API reference: every named GraphQL operation.