Openfront Salon

Services, stylists, booking, schedules, resources, checkout, refunds, memberships, gift cards, loyalty, and commissions.

Source · Live site · Catalog

Openfront Salon is a self-hostable salon app. Clients browse services and stylists, book appointments, and manage their visits in an account. Salon staff use the dashboard to run the day: bookings, check-in, consultations, checkout, refunds, schedules, resources, retail stock, memberships, packages, gift cards, loyalty, reviews, commissions, and reports.

It is built on Next.js 16, React 19, Keystone 6 (GraphQL), Prisma, and PostgreSQL. Start with Getting started, then read the dashboard pages for the area you run.

The app runs one salon business. It has no organization or tenant model. Role flags apply to every location.

Who uses what

UserSurfaceTypical jobs
ClientStorefront and /accountBrowse services, book, cancel or reschedule, sign consultation forms, leave reviews, see points and gift cards
ReceptionAppointments, checkout, waitlistBook, check in, take payment
StylistSchedules, staff profileSet own hours, see upcoming work and earnings
ManagerEverythingCatalog, staff, refunds, reports
FinanceSales, commissions, reportsRefunds, payouts

Surfaces

  • Storefront at /: home, services, team, booking, account, memberships, gift cards, gallery, reviews, and contact. See External storefronts.
  • Dashboard at /dashboard: salon pages under /dashboard/platform/* plus the generic Keystone list screens at /dashboard/[listKey]. See External dashboards.
  • GraphQL at /api/graphql.
  • Payment webhooks at /api/payments/webhooks/[provider].
  • Dashboard assistant at /api/completion, backed by an MCP endpoint at /api/mcp-transport/[transport].

Models

The schema has 58 lists in features/keystone/models. Most business lists block generated create, update, and delete. They change only through custom mutations that check the role, run in a serializable transaction, and take an idempotency key. Money is stored as integer cents. Times are instants; the salon's local time comes from Location.timezone.

AreaLists
IdentityUser, Role
CatalogServiceCategory, Service, ServiceVariant, ServiceSegment, SalonImage, Product, InventoryMovement, ProductSale (legacy)
People and capacityLocation, StoreSettings, Stylist, StylistSchedule, SalonResource
Clients and careClient, ClientNote, ColorFormula, FormTemplate, ConsultationTemplateVersion, ConsultationSubmission, ConsultationReview, Review, IntakeForm and FormSubmission (legacy)
BookingAppointment, AppointmentLineItem, AppointmentScheduleRevision, AppointmentSegment, SegmentTransition, WaitlistEntry
MoneySalonTransaction, SalonTransactionLineItem, Payment, PaymentProvider, PaymentSession, PaymentWebhookEvent, TransactionReversal, TransactionReversalLine, Commission, CommissionAdjustment, GratuityLedgerEntry, DepositAdjustment
Benefits and loyaltyMembership, ClientMembership, ServicePackage, ClientPackage, ClientPackageUsage, ClientPackageAdjustment, GiftCard, GiftCardTransaction, LoyaltyProgram, ClientPoints, Referral
Messages and systemMessageTemplate, MessageLog, DomainEvent, WidgetConfiguration

Revisions, transitions, consultation reviews, ledger entries, reversals, and adjustments are append-only. A correction is a new row. The source repo keeps a fuller map in docs/schema-map.md.

Tenancy and roles

There is one salon per deployment. Location is a site of that salon, not a tenant. StoreSettings is a single row for storefront identity. There is no organization list and no per-location staff membership.

Access comes from boolean flags on Role: canAccessDashboard, canManageSalon, canManageAppointments, canManageClients, canManageCareRecords, canManageCatalog, canManageSchedules, canManageOwnSchedule, canManageResources, canManageInventory, canManagePayments, canIssueRefunds, canManageCommissions, canViewReports, canManageIntegrations, canManageOnboarding, and the people/role flags.

  • canManageSalon acts as a manager override for most flags.
  • canManageCareRecords and canManageOnboarding must be set on their own. The manager flag does not grant them.
  • Signed-in clients read their own clients, appointments, payments, benefits, points, and gift cards through owner filters.
  • Anyone can read active locations. Managers also see inactive ones.
  • With more than one active location, booking needs an explicit locationId.

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

Custom GraphQL operations

Registered in features/keystone/mutations/index.ts.

Queries: checkAvailability, calculateCommission, storefrontStylists, storefrontStylist, salonOperationsWorkspace, salonManagementWorkspace, salonLocationStock, clientPortal, publicStorefrontSettings, storefrontBenefits, giftCardBalance, redirectToInit.

Mutations:

  • Accounts: registerSalonCustomer.
  • Booking: bookAppointment, transitionAppointment, scheduleAppointmentSegments, transitionAppointmentSegment, processAppointmentDeposit.
  • Waitlist: joinWaitlist, joinWaitlistAsClient, transitionWaitlistEntry.
  • Schedules and resources: upsertStylistSchedule, setStylistScheduleAvailability, setSalonResourceAvailability.
  • Checkout and payments: completeAppointmentCheckout, processPayment, confirmPayment, refundPayment, distributeTip.
  • Refunds and corrections: reverseSalonTransaction, adjustSaleCompensation.
  • Payouts: payoutCommissions, payoutGratuities.
  • Benefits: sellClientBenefit, setClientBenefitStatus, redeemClientPackage.
  • Gift cards: purchaseGiftCard, confirmGiftCardPurchase, applyGiftCardBalance.
  • Loyalty: adjustClientPoints, completeReferral.
  • Care and consultations: updateClientCareProfile, submitAppointmentReview, publishConsultationTemplateVersion, submitConsultation, reviewConsultation.
  • Stock: adjustSalonInventory.
  • Setup: upsertSalonStoreSettings, saveSalonRecord (locations, resources, staff, categories, services, products), runSalonOnboarding.

Some dashboard pages still use generated CRUD for simpler lists: membership and package plans, loyalty programs, referrals, review moderation, client notes, color formulas, form templates, and image uploads.

Main workflow

  1. Set up the salon: storefront identity, locations and hours, resources, staff and roles.
  2. Build the menu: categories, services (price, duration, deposit, consultation and patch-test flags, specialists, resources).
  3. Set weekly hours and time off.
  4. Take a booking online or at the desk. The booking stores a service snapshot, segments, and a schedule revision.
  5. Publish consultation forms. Clients sign. Staff review before service.
  6. Check in, work each step, then complete or mark no-show.
  7. Check out: services, retail, discount, tip, package or membership credit. This writes the sale, payment, stock movements, commissions, tip allocations, and loyalty points.
  8. Refund, void, or partly refund a sale. Correct commission and tips.
  9. Pay out commissions and card tips.

Onboarding

New dashboard users see onboarding cards in the sidebar. The dialog offers three templates: Complete Salon Setup, Essential Booking Setup, and Custom Setup (edit the seed JSON and paste it back). All three call runSalonOnboarding with a seed built from features/platform/onboarding/lib/seed.json.

The seed creates a sample salon, "Morrow & Mane": store settings, a location, resources, categories, services, stylists and schedules, two clients, a consultation form, membership plans, a package, a product, a message template, two appointments, gallery images, and two payment providers (manual, installed and default; Stripe, not installed).

The mutation needs canManageOnboarding. It runs in one transaction and reports how many records it created and reused, so a second run reuses existing rows. Replace the sample names, prices, and hours with your own before you open for booking.

Payments and integrations

  • Payment adapters: manual and stripe, in features/integrations/payment. Desk checkout, recorded deposits, and gift-card sales use the installed manual provider. A manual payment records tender taken at the desk.
  • Stripe: reads STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET. The webhook route takes a provider code like pp_stripe, checks the signature, logs each event once, rejects amount or currency mismatches, and handles payment_intent.succeeded, payment_intent.payment_failed, payment_intent.canceled, charge.refunded, and refund.updated.
  • Email: booking confirmations, gift-card emails, and password resets are sent with SMTP only when SMTP_HOST and SMTP_FROM are set. Without SMTP, nothing is sent and the dashboard says reset email is unavailable.
  • File storage: uploads use S3-compatible storage (S3_* variables). Bundled storefront images under /images do not need it.
  • Dashboard assistant: /api/completion uses OpenRouter. It needs a signed-in user with canAccessDashboard, plus OPENROUTER_API_KEY and OPENROUTER_MODEL, or a key the user enters locally. Its MCP endpoint exposes generic list tools (listModels, queryData, searchModels, getFieldsForType, lookupInputType, createData, updateData, deleteData, modelSpecificSearch) that run as the caller's session.

On this page