Skip to main content
@voyant-travel/cruises is the cruise vertical for OTA, tour-operator, and DMC deployments. It models cruise inventory the way the industry actually describes it (sailings, ships, decks, cabin grades, fare codes, occupancy, onboard credit) instead of bending cruises into the per-person, per-departure shape of the products module. Cruises are an inventory and operations capability inside those scenarios, not a separate cruise-line implementation. The module ships the canonical schema, services, admin and storefront routes, and a booking extension, plus a provider-neutral adapter contract so external cruise inventory can flow through the same surfaces as cruises an operator owns. The package is strictly opt-in. Deployments that do not sell cruises get no cruise tables, no cruise routes, and no cruise types in their TypeScript surface.

Key concepts

These terms come from the shared glossary. Cruises uses them natively rather than inventing synonyms.
  • Cruise catalog. A cruise is the route template (the voyage, itinerary, or holiday a line publishes). It carries the name, ship, nights, embark and disembark ports, and merchandising copy. The cruise is canonical, module-owned truth for self-managed inventory, and a derived read model for sourced inventory.
  • Sailings. A sailing is a dated departure of a cruise on a specific ship, with departure and return dates, a sales status (open, on request, wait list, sold out, closed), and its own pricing. It is the equivalent of a Slot in the products world, but cruise availability lives per price row rather than per product day.
  • Cabin categories. A cabin category is the archetype (Veranda Suite, Oceanview) on a ship, with a canonical room type, occupancy bounds, amenities, and the grade codes that map to it. Most operators sell at the category level. Specific cabins (cabin numbers) are an optional second layer for “guarantee a forward-facing balcony” inventory.
  • Itinerary stops. The day-by-day itinerary lives at two levels: a per-cruise template and per-sailing overrides for sailings that skip a port or reroute. Reading the effective itinerary merges the two. Ports reference shared Places (facility rows) plus canonical place ids for faceting.
  • Cruise extensions. A Cruise Extension is a cruise-specific pre or post-cruise hotel or land program. Its offer definition can be reused across many sailings. When the extension is cruise-owned and lifecycle-dependent it travels as an Extra under the cruise booking; when it is independently supplied, confirmed, cancelled, taxed, or supported it splits into a sibling Component Booking under the same Trip Envelope.
  • The cruise adapter contract. External cruises reach the module through a registered CruiseAdapter. The adapter is swappable, not baked in. A deployment picks the adapter package at startup; the framework never imports a concrete adapter. This is how Connect cruises plug in: a thin adapter over the Connect SDK satisfies the same contract a self-managed cruise resolves against.

Provenance: self-managed and external

Every cruise the operator’s customer sees comes from one of two places, side by side in the same admin experience.
  • Self-managed cruises live in the operator’s own database under the full canonical schema, with full admin CRUD. Bookings against them never touch an upstream.
  • External cruises live in an upstream system reached through an adapter. The local database stores nothing about the cruise itself; reads resolve live through the adapter, and bookings snapshot the upstream state into the local booking row.
The admin list interleaves both with an External badge. Bookings, payments, CRM, and finance plumbing is identical across the two.

What it owns

  • The canonical cruise schema: cruises, sailings, ships, decks, cabin categories, specific cabins, prices, price components, itinerary days, per-sailing day overrides, media, inclusions, voyage groups (back-to-back, grand, world, and cruise-tour composites), and an opt-in storefront search index.
  • A pricing grid keyed by (sailing, cabin category, occupancy) with fare codes, onboard credit, gratuities, port charges, taxes, and NCF modeled as composable price components.
  • cruisesService for cruise, sailing, ship, cabin, and itinerary reads and writes, and pricingService for quote assembly and lowest-price aggregates.
  • Admin routes (/v1/admin/cruises/*) that are provenance-aware, and storefront routes (/v1/public/cruises/*) that read from the search-index projection.
  • A booking extension (booking_cruise_details) plus a multi-cabin party-booking path that composes the bookings module’s group primitives.
  • The CruiseAdapter contract, the adapter registry, a memoizing decorator, a MockCruiseAdapter for tests, and assertCruiseAdapterCompatibility for adapter authors.
What it does not own: cruise-line connectors (those live in Connect or an operator’s own adapter), sync scheduling and polling, provider authentication, and real-time hold or live-book orchestration beyond the adapter commit boundary.

Working with it

Create and price a self-managed cruise

import { cruisesService } from "@voyant-travel/cruises"
import { pricingService } from "@voyant-travel/cruises/service"

const cruise = await cruisesService.createCruise(db, {
  slug: "danube-symphony",
  name: "Danube Symphony",
  cruiseType: "river",
  nights: 7,
})

const sailing = await cruisesService.upsertSailing(db, {
  cruiseId: cruise.id,
  shipId,
  departureDate: "2026-05-12",
  returnDate: "2026-05-19",
  salesStatus: "open",
})

// Bulk-replace the pricing grid for this sailing in one transaction.
await cruisesService.upsertPrices(db, sailing.id, [
  { cabinCategoryId, occupancy: 2, fareCode: "EARLY_BIRD", currency: "EUR", pricePerPerson: "2495.00" },
  { cabinCategoryId, occupancy: 1, fareCode: "EARLY_BIRD", currency: "EUR", pricePerPerson: "3245.00" },
])

Assemble a quote

assembleQuote is the single place quote math happens. The booking extension, the storefront, and the admin UI all call it, so there is one source of truth. Quotes return the native currency of the underlying price rows; the module never converts at ingest time.
const quote = await pricingService.assembleQuote(db, {
  sailingId: sailing.id,
  cabinCategoryId,
  occupancy: 2,
  guestCount: 2,
})
// quote: base price, applied price components, total per person,
// total per cabin, and the native currency that was quoted.

Read the effective itinerary

const itinerary = await cruisesService.getEffectiveItinerary(db, sailing.id)
// Merges cruise_days (template) with cruise_sailing_days (per-sailing overrides),
// so skipped ports and reroutes for this departure are reflected.

Book a cabin

A cruise booking is a booking. The extension wraps the bookings module so the quote, the booking row, and the cruise detail snapshot all land in one transaction. The mode flag chooses between lead-gen inquiry and a payment-capable reserve, per tenant and per line.
import { cruisesBookingService } from "@voyant-travel/cruises/booking-extension"

const booking = await cruisesBookingService.createCruiseBooking(db, {
  sailingId: sailing.id,
  cabinCategoryId,
  occupancy: 2,
  fareCode: "EARLY_BIRD",
  passengers: [
    { firstName: "Ada", lastName: "Byron", travelerCategory: "adult", isPrimary: true },
    { firstName: "George", lastName: "Byron", travelerCategory: "adult" },
  ],
  contact: { personId },
  mode: "reserve",
})
For a family booking several cabins on the same sailing as one logical purchase, createCruisePartyBooking composes the bookings group primitives: one shared confirmation number, atomic cancellation across cabins, a single deposit and invoice, with each cabin keeping its own travelers and quote snapshot.

Register an external adapter

The adapter is wired at the deployment edge. The framework package stays provider-neutral.
import { createApp } from "@voyant-travel/hono"
import { cruisesHonoModule, registerCruiseAdapter } from "@voyant-travel/cruises"
import { memoizeCruiseAdapter } from "@voyant-travel/cruises/adapters"
import { createCruiseAdapter } from "external-cruise-adapter"

registerCruiseAdapter(
  memoizeCruiseAdapter(createCruiseAdapter({ token: env.CRUISE_ADAPTER_TOKEN }), { ttlMs: 60_000 }),
)

export const app = createApp({ modules: [cruisesHonoModule] })
Once registered, admin list and detail routes interleave local cruises (read from the database) with external cruises (read live through the adapter). Booking creation detects provenance from the sailing key, commits upstream through adapter.createBooking() first, then snapshots the result. The caller does not branch; the helper does.
  • bookings. The booking_cruise_details extension and the booking_group_cruise_details party extension plug into the bookings state machine, PII handling, and financial linkage.
  • pricing. Dated promotions and onboard-credit overlays reuse pricing.price_catalogs and pricing.price_schedules via soft foreign keys on cruise price rows. There is no cruise-local promotions table.
  • finance. Bookings become invoices through the finance module; cruise line items live in the booking quote snapshot, not in finance.
  • suppliers and Places. Cruise lines are supplier rows linked from cruises.lineSupplierId; ports are facility (Place) rows referenced by soft foreign key.
  • products. Pre and post-cruise extensions that are reusable sellable products link through a template-level link rather than a cross-package foreign key.
  • Connect. Connect cruises is the default source of external cruise inventory. A thin adapter over the Connect SDK implements the CruiseAdapter contract; from the module’s point of view it is just another registered adapter.

React package

@voyant-travel/cruises-react is the cruises client tier: headless data hooks and a Zod-validated fetch client at the root and ./hooks, ./client, and ./query-keys subpaths, plus styled UI under ./ui and ./components/*. The headless subpaths pull no styling peers; the styled subpaths add @voyant-travel/ui.
import { VoyantCruisesProvider } from "@voyant-travel/cruises-react/provider"
import { useStorefrontCruises } from "@voyant-travel/cruises-react"

function CruiseGrid() {
  const { data, isLoading } = useStorefrontCruises({ cruiseType: "expedition" })
  if (isLoading) return <p>Loading</p>
  return (
    <ul>
      {data?.data.map((cruise) => (
        <li key={cruise.id}>{cruise.name}</li>
      ))}
    </ul>
  )
}

function App() {
  return (
    <VoyantCruisesProvider baseUrl="/api">
      <CruiseGrid />
    </VoyantCruisesProvider>
  )
}
Components render English by default. Wrap them in CruisesUiMessagesProvider and import only the locales you support (for example ./i18n/en and ./i18n/ro) to localize.

Next steps

Connect cruises

How sourced cruise inventory reaches the adapter contract.

Connect adapter

The contract external inventory implements to plug in.

Charters

The sister module for per-suite and whole-yacht voyages.

Glossary

The cruise vocabulary in the shared domain language.