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 StationAssignment for the flight's origin airport with the matching flag: canCheckIn, canAcceptBags, or canBoard.

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.

AreaLists
Identity and accessUser, Role, Carrier, ApprovedAgent, StationAssignment, MachineCredential, OnboardingStep
Network and scheduleAirport, Route, FlightSchedule, FlightInstance, SeatMap, Seat
Product and pricingCabin, FareBrand, FareRule, AncillaryProduct, InventoryBucket, InventoryHold
Retailing and ordersOffer, OfferItem, Passenger, Booking, BookingPassenger, Journey, BookingSegment, BookingLineItem, ServiceRequest
TicketingTicket, TicketCoupon, ServiceObligation, TicketCouponLineage, ElectronicMiscDocument, ElectronicMiscCoupon, Exchange
PaymentsPaymentProvider, PaymentSession, Payment, ManualPaymentEvidence, Refund, SettlementLedgerEntry
Airport serviceTravelDocument, DocumentVerification, SeatAssignment, CheckIn, BoardingPass, Bag, BagEvent
Irregular operationsDisruption, Reaccommodation
Integrations and systemIntegrationProvider, 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 by captureBookingPayment ("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.

GroupQueriesMutations
Storefront (public)publicCarrierIdentity, publicAirlineFlightOptions, publicFlightStatuses, customerBookingcreateCustomerOffer, createCustomerBooking, startCustomerCheckout, completeCustomerCheckout, assignCustomerSeat, checkInCustomerBooking, cancelCustomerBooking, rotateCustomerBookingAccessToken, recoverCustomerBookingAccess
SetupmachineCredentialSummariescompleteCarrierOnboarding, dismissCarrierOnboarding, updateCarrierStorefrontSettings, issueMachineCredential, revokeMachineCredential
Network and inventoryairlineFlightOptionsregisterAirport, provisionDatedFlight, setDatedFlightSalesStatus, setInventoryAuthorization, updateFlightOperations
Offers and bookingsoperatorBookingReceiptcreateOfferFromShopping, createBookingFromOffer, cancelBooking, reissueBookingAccessCode
Payments and ticketingrecordReceivedPayment, submitManualPaymentEvidence, approveManualPaymentEvidence, rejectManualPaymentEvidence, captureBookingPayment, issueBookingTickets, processBookingRefund
Airport servicedepartureManifestregisterPassengerTravelDocument, verifyPassengerTravelDocument, checkInBookingPassenger, boardBookingPassenger, offloadBookingPassenger, acceptCheckedBag, recordBagEvent
Recovery and systempublishFlightDisruption, 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) and stripe (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 applies checkout.session.completed events. The secret comes from an env variable named AIRLINE_WEBHOOK_SECRET_<NAME> set in the provider's webhookSecretEnv configuration. 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 audience settlement-import and scope settlements:write.
  • Outbound webhooks. The worker posts outbox events to active WebhookEndpoint records that subscribe to the topic. Hosts must be listed in OUTBOUND_WEBHOOK_HOSTS.
  • Email. SMTP sends password resets and booking access recovery. Without SMTP_HOST and SMTP_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-transport offers generic tools (listModels, queryData, searchModels, getFieldsForType, lookupInputType, createData, updateData, deleteData, modelSpecificSearch) that run as the signed-in user. In production it returns 404 unless AI_ASSISTANT_ENABLED=true.

Where to go next

On this page