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
| Concept | Meaning |
|---|---|
| Room type | What guests book: rate, occupancy, bed configuration, images |
| Room | A physical room of one type, with a floor and operational status |
| Rate plan | Price rules for a room type: stay limits, cancellation policy, meal plan, dates, promo code |
| Booking | A reservation with snapshot amounts in minor units. Written only by named commands |
| Folio | The ledger for a booking or group. Entries are posted and reversed, never edited |
| Business date | The date the property is trading on. Night audit moves it forward |
| Approval | A second user with canApproveHotelExceptions authorizes refunds, write-offs, and similar exceptions |
| Idempotency key | Sent with most staff mutations so a retry does not repeat the action |
Code layout
| Path | What 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.ts | Custom 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:
| Area | Lists |
|---|---|
| People and access | User, Role |
| Property | HotelSettings (singleton), HotelBusinessDate, NightAuditRun, DailyMetrics |
| Rooms and rates | RoomType, RoomImage, Room, RoomInventory, RatePlan, SeasonalRate |
| Reservations | Booking, ReservationLineItem, RoomAssignment, BookingModificationRequest |
| Guests | Guest, GuestDocument, LoyaltyTransaction |
| Payments | PaymentProvider, BookingPaymentSession, BookingPayment, PaymentEvent, RefundIntent |
| Folios | Folio, FolioEntry |
| Operations | HousekeepingTask, MaintenanceRequest, GroupBlock, GroupBlockAllocation |
| Channels | Channel, ChannelReservation, ChannelSyncEvent |
| Evidence and runtime | HotelAuditEvent, 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:
| Area | Mutations |
|---|---|
| Reservations | createStaffBooking, amendStaffBooking, updateBookingStatus, updateBookingStayDates, assignRoomToBooking, resolveBookingModificationRequest, resolveOverdueCheckedInBooking, ensureReservationSnapshots, ensureGuestBookingAccess, updateHotelStayRegister, updateHotelRelocation |
| Rooms and rates | updateRoomOperationalStatus, updateHotelRoomOutage, updateRoomInventoryControls, updateRatePlanPublication, updateHotelDerivedRate |
| Housekeeping and maintenance | scheduleHousekeepingTask, updateHousekeepingTaskStatus, updateHotelHousekeepingStaffCapability, reportRoomMaintenanceIssue, updateMaintenanceRequestStatus, updateHotelMaintenanceCommercial, updateHotelStayService |
| Folios and money | postFolioEntry, reverseFolioEntry, recordBookingPayment, closeReconciledFolio, requestBookingPaymentRefund, replayRefundIntent, manageHotelCashier, manageHotelReceivable, manageHotelPayout, annotateHotelDispute, manageHotelSecurityAuthorization, redeemHotelLoyalty |
| Groups | createHotelGroupBlock, updateHotelGroupBlockStatus, pickupHotelGroupBlock, createHotelGroupRoomingList, detachHotelGroupBooking, closeHotelGroupMasterFolio |
| Property and setup | runHotelOnboarding, updateHotelPropertySettings, runHotelNightAudit, configureHotelPaymentProvider |
| Governance and security | updateHotelGuestGovernance, updateHotelApproval, verifyHotelMfa, manageHotelMfa |
| Channels and outbox | saveHotelChannelDraft, 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, andHOTEL_OUTBOX_DISPATCH_CREDENTIAL_KEY_IDare 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/healthreturns a static OK./api/readychecks settings, business date, providers, and worker progress./api/hotel/outbox/receiveraccepts HMAC-signed events only whenHOTEL_OUTBOX_RECEIVER_SECRETandHOTEL_OUTBOX_RECEIVER_CREDENTIAL_KEY_IDare set./api/completionis the dashboard AI chat. It needs a signed-in user withcanAccessDashboardplusOPENROUTER_API_KEYandOPENROUTER_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-intentand/api/send-booking-confirmationreturn HTTP 410. Use the booking payment session and outbox paths instead.