Openfront Hotel

Single-property room inventory, direct booking, guest access, front desk, folios, housekeeping, maintenance, groups, and payments.

Source · Live site · Catalog

Openfront Hotel is a self-hostable hotel property-management and direct-booking application for one property. Guests browse rooms, get a stay quote, book direct, and review their booking and folio. Staff use the dashboard for reservations, room assignment, rates, housekeeping, maintenance, folios, cashier work, receivables, group blocks, and night audit.

It is built with Next.js 16, React 19, Keystone 6, Prisma, and PostgreSQL. Start with Getting started.

One database holds one property. There is no Organization, Property, or Location model. HotelSettings is a singleton row, roles are global, and the property key the-alder-house is a fixed constant used to label audit, outbox, business-date, and night-audit records. There are no native Booking.com or Expedia adapters.

Concepts

ConceptMeaning
Room typeWhat guests book: rate, occupancy, bed configuration, images
RoomA physical room of one type, with a floor and operational status
Rate planPrice rules for a room type: stay limits, cancellation policy, meal plan, dates, promo code
BookingA reservation with snapshot amounts in minor units. Written only by named commands
FolioThe ledger for a booking or group. Entries are posted and reversed, never edited
Business dateThe date the property is trading on. Night audit moves it forward
ApprovalA second user with canApproveHotelExceptions authorizes refunds, write-offs, and similar exceptions
Idempotency keySent with most staff mutations so a retry does not repeat the action

Code layout

PathWhat it holds
app/(storefront)/**Guest route entry points
app/dashboard/(admin)/platform/**Staff workspace route entry points
app/api/**GraphQL, payment and channel webhooks, outbox receiver, health, readiness, dashboard AI
features/keystone/models/**40 Keystone lists
features/keystone/mutations/index.tsCustom GraphQL schema: named queries and mutations
features/keystone/queries/**Storefront and guest query resolvers, plus hotelOperations.ts for staff workspace projections
features/keystone/{bookings,cashier,channels,communications,finance,folios,groups,guest-governance,inventory,loyalty,operations,payments,rates,receivables,refunds,reporting,security,workforce}Business commands behind those operations
features/keystone/jobs/**In-process workers started from instrumentation.ts
features/platform/**Staff workspaces and onboarding
features/storefront/**Guest screens, components, server actions
features/integrations/payment/**Stripe and PayPal adapters

Models

The registry in features/keystone/models/index.ts has these lists:

AreaLists
People and accessUser, Role
PropertyHotelSettings (singleton), HotelBusinessDate, NightAuditRun, DailyMetrics
Rooms and ratesRoomType, RoomImage, Room, RoomInventory, RatePlan, SeasonalRate
ReservationsBooking, ReservationLineItem, RoomAssignment, BookingModificationRequest
GuestsGuest, GuestDocument, LoyaltyTransaction
PaymentsPaymentProvider, BookingPaymentSession, BookingPayment, PaymentEvent, RefundIntent
FoliosFolio, FolioEntry
OperationsHousekeepingTask, MaintenanceRequest, GroupBlock, GroupBlockAllocation
ChannelsChannel, ChannelReservation, ChannelSyncEvent
Evidence and runtimeHotelAuditEvent, HotelOutboxEvent, HotelOutboxAttempt, HotelOutboxReceipt, HotelSeedRecord, HotelAbuseBucket, HotelWorkerLease

Some features have no list of their own. Cashier shifts, receivable accounts and invoices, approvals, stay services, room outages, relocations, the stay register, and guest-governance requests are stored as HotelAuditEvent rows with an aggregateType, and read back through named queries.

Booking, ReservationLineItem, and FolioEntry deny create, update, and delete through generated CRUD. Changes go through named commands. Bookings store snapshot amounts in integer minor units (totalAmountMinor, balanceDueMinor, and so on). Older Float fields such as totalAmount and roomRate still exist on Booking, so the schema is not fully fixed-point.

Roles

The first user created at /dashboard/init gets an Admin role with every flag. Role flags are:

canAccessDashboard, canManageRooms, canManageBookings, canManageHousekeeping, canManageGuests, canManagePayments, canSeeOtherPeople, canEditOtherPeople, canManagePeople, canManageRoles, canManageOnboarding, canManageAudit, canManageGuestPrivacy, canApproveHotelExceptions, canManageIntegrations.

Staff can enable TOTP MFA with recovery codes at /dashboard/platform/security. Guest accounts are User rows with no role. Dashboard sign-up is closed unless PUBLIC_SIGNUPS_ALLOWED=true.

Named GraphQL operations

All are defined in features/keystone/mutations/index.ts or merged into it.

Guest and storefront queries: publicHotelSettings, storefrontRoomTypes, storefrontRoomType, storefrontAvailability, storefrontStayCalendar, storefrontQuote, bookingPaymentProviders, activeBookingPaymentSession, guestBooking, guestBookings, guestFolio, guestCancellationQuote, guestAccountStatus, guestAccountBookings, guestAccountBookingsCount, hotelSecurityAuthorization.

Guest and storefront mutations: verifyGuestBooking, createStorefrontBooking, initiateBookingPaymentSession, completeBookingPayment, cancelBooking, requestBookingModification, submitHotelContactMessage, registerGuestAccount, claimGuestAccountStays, updateGuestAccount, changeGuestAccountPassword, redeemHotelPasswordResetToken.

Staff workspace queries: hotelOperatorCapabilities, hotelFrontDesk, hotelReservationCalendar, hotelRoomOperations, hotelRateOperations, hotelDerivedRateWorkspace, hotelHousekeepingOperations, hotelHousekeepingStaffCapabilities, hotelMaintenanceOperations, hotelMaintenanceCommercial, hotelFolioOperations, hotelFolioReceipt, hotelPayerWindows, hotelNightAuditOperations, hotelPaymentOperations, hotelRefundQuote, hotelPaymentProviderOperations, hotelPayoutOperations, hotelCashierOperations, hotelReceivableOperationsPage (hotelReceivableOperations is deprecated), hotelDisputeOperations, hotelGroupOperations, hotelGroupWorkspace, hotelGuestOperations, hotelGuestMessages, hotelGuestGovernance, hotelGovernanceGuestSearch, hotelApprovalWorkspace, hotelChannelOperations, hotelAnalyticsOperations, hotelOutboxOperations, hotelLoyaltyAccount, hotelRelocation, hotelStayServices, hotelRoomOutages, hotelStayRegister, hotelMfaStatus.

Staff mutations, by area:

AreaMutations
ReservationscreateStaffBooking, amendStaffBooking, updateBookingStatus, updateBookingStayDates, assignRoomToBooking, resolveBookingModificationRequest, resolveOverdueCheckedInBooking, ensureReservationSnapshots, ensureGuestBookingAccess, updateHotelStayRegister, updateHotelRelocation
Rooms and ratesupdateRoomOperationalStatus, updateHotelRoomOutage, updateRoomInventoryControls, updateRatePlanPublication, updateHotelDerivedRate
Housekeeping and maintenancescheduleHousekeepingTask, updateHousekeepingTaskStatus, updateHotelHousekeepingStaffCapability, reportRoomMaintenanceIssue, updateMaintenanceRequestStatus, updateHotelMaintenanceCommercial, updateHotelStayService
Folios and moneypostFolioEntry, reverseFolioEntry, recordBookingPayment, closeReconciledFolio, requestBookingPaymentRefund, replayRefundIntent, manageHotelCashier, manageHotelReceivable, manageHotelPayout, annotateHotelDispute, manageHotelSecurityAuthorization, redeemHotelLoyalty
GroupscreateHotelGroupBlock, updateHotelGroupBlockStatus, pickupHotelGroupBlock, createHotelGroupRoomingList, detachHotelGroupBooking, closeHotelGroupMasterFolio
Property and setuprunHotelOnboarding, updateHotelPropertySettings, runHotelNightAudit, configureHotelPaymentProvider
Governance and securityupdateHotelGuestGovernance, updateHotelApproval, verifyHotelMfa, manageHotelMfa
Channels and outboxsaveHotelChannelDraft, pushInventoryToChannel, pullReservationsFromChannel, retryFailedChannelSyncs, replayHotelOutboxEvent

Most staff mutations take an idempotencyKey. Refunds, write-offs, cash variances, and rate publication can require an approval from a second user with canApproveHotelExceptions, depending on thresholds in property settings.

Guest access

A guest proves access with confirmation number and email (verifyGuestBooking) or by signing in to a guest account. Access is kept in an HMAC-signed hotel-guest-access cookie that lasts 30 days and holds at most 20 bookings. It is signed with GUEST_ACCESS_SECRET, falling back to SESSION_SECRET and then NEXTAUTH_SECRET. Guest queries recheck the cookie on every call.

A storefront booking starts as a pending hold that expires after 30 minutes. A worker cancels expired holds. The booking needs a signed quote from storefrontQuote (signed with HOTEL_QUOTE_SECRET).

Onboarding

runHotelOnboarding needs canManageOnboarding. It runs in one serializable transaction under a Postgres advisory lock. Templates:

  • full — The Alder House: 3 room types, 8 rooms, 4 rate plans, 2 seasonal rates, 3 guests, 3 bookings, 2 payments, housekeeping and maintenance rows, 2 channels, loyalty, and inventory.
  • minimal — a smaller subset: 2 room types, 5 rooms, 2 rate plans, 1 seasonal rate, 1 guest, 1 booking and payment, 1 housekeeping task, 1 maintenance request, 1 channel, and 2 inventory rows.
  • custom — your own JSON, checked against the onboarding schema.

For full and minimal, seed dates are moved relative to the property business date; custom dates are kept as given. Seeded channels (named Booking.com and Expedia) are inactive and are not in live mode, so they never call a remote endpoint. Onboarding refuses to run if the database already has rooms, room types, bookings, folios, housekeeping tasks, maintenance requests, guests, rate plans, channels, inventory rows, night audits, or seed records. Running it again with the same input is a no-op; with different input it fails. After setup, change data through property settings and the workspaces.

Payments

Two payment adapters are registered: Stripe (pp_stripe_stripe) and PayPal (pp_paypal_paypal). Configure them at /dashboard/platform/payment-providers (needs canManagePayments and canManageIntegrations). Credentials are encrypted with HOTEL_DATA_ENCRYPTION_KEY and are not shown again. A Stripe record with no stored credentials can fall back to STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, and STRIPE_WEBHOOK_SECRET when STRIPE_ENABLED=true.

Provider webhooks enter at /api/payment-providers/[providerCode]/webhook. Staff can record offline payments on a folio with recordBookingPayment. Stripe disputes are tracked at /dashboard/platform/disputes; evidence is submitted in the Stripe dashboard, not from this app.

Channels

Channel support is a generic HTTP bridge, not an OTA integration. Each Channel stores a mode in its credentials: disabled or live (or a non-sending mode). Only live channels send requests. A live channel needs HTTPS endpoints whose origins are listed in allowedOrigins. Inbound reservations arrive at /api/webhooks/channel/[channelId], which requires live mode, an HMAC signature header, and a signed event id used for replay protection. Channel sync pushes inventory and pulls reservations. The dashboard Channels page says channels are off by default.

Workers and outbox

instrumentation.ts starts four in-process workers in the Node.js runtime (not during build or tests):

  • channel sync: inventory push every 15 minutes, reservation pull every 5 minutes, retries every 2 minutes, live channels only;
  • outbox delivery: starts only when SMTP is fully configured or HOTEL_OUTBOX_DISPATCH_URL, HOTEL_OUTBOX_DISPATCH_SECRET, and HOTEL_OUTBOX_DISPATCH_CREDENTIAL_KEY_ID are all set;
  • refund intents;
  • expiry of booking holds and group block release dates.

Workers take database leases (HotelWorkerLease), so only one process runs each job at a time. Guest emails (confirmation, update, cancellation, refund, no-show, pre-arrival, modification response, contact receipt) are queued as outbox events. Without SMTP or an HTTP dispatcher, they stay pending.

Other endpoints

  • /api/health returns a static OK. /api/ready checks settings, business date, providers, and worker progress.
  • /api/hotel/outbox/receiver accepts HMAC-signed events only when HOTEL_OUTBOX_RECEIVER_SECRET and HOTEL_OUTBOX_RECEIVER_CREDENTIAL_KEY_ID are set.
  • /api/completion is the dashboard AI chat. It needs a signed-in user with canAccessDashboard plus OPENROUTER_API_KEY and OPENROUTER_MODEL.
  • /api/mcp-transport/[transport] returns HTTP 410. MCP machine access is disabled until scoped machine authentication exists, so the dashboard AI chat has no tools to call.
  • /api/create-payment-intent and /api/send-booking-confirmation return HTTP 410. Use the booking payment session and outbox paths instead.

On this page