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
| User | Surface | Typical jobs |
|---|---|---|
| Client | Storefront and /account | Browse services, book, cancel or reschedule, sign consultation forms, leave reviews, see points and gift cards |
| Reception | Appointments, checkout, waitlist | Book, check in, take payment |
| Stylist | Schedules, staff profile | Set own hours, see upcoming work and earnings |
| Manager | Everything | Catalog, staff, refunds, reports |
| Finance | Sales, commissions, reports | Refunds, 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.
| Area | Lists |
|---|---|
| Identity | User, Role |
| Catalog | ServiceCategory, Service, ServiceVariant, ServiceSegment, SalonImage, Product, InventoryMovement, ProductSale (legacy) |
| People and capacity | Location, StoreSettings, Stylist, StylistSchedule, SalonResource |
| Clients and care | Client, ClientNote, ColorFormula, FormTemplate, ConsultationTemplateVersion, ConsultationSubmission, ConsultationReview, Review, IntakeForm and FormSubmission (legacy) |
| Booking | Appointment, AppointmentLineItem, AppointmentScheduleRevision, AppointmentSegment, SegmentTransition, WaitlistEntry |
| Money | SalonTransaction, SalonTransactionLineItem, Payment, PaymentProvider, PaymentSession, PaymentWebhookEvent, TransactionReversal, TransactionReversalLine, Commission, CommissionAdjustment, GratuityLedgerEntry, DepositAdjustment |
| Benefits and loyalty | Membership, ClientMembership, ServicePackage, ClientPackage, ClientPackageUsage, ClientPackageAdjustment, GiftCard, GiftCardTransaction, LoyaltyProgram, ClientPoints, Referral |
| Messages and system | MessageTemplate, 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.
canManageSalonacts as a manager override for most flags.canManageCareRecordsandcanManageOnboardingmust 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
- Set up the salon: storefront identity, locations and hours, resources, staff and roles.
- Build the menu: categories, services (price, duration, deposit, consultation and patch-test flags, specialists, resources).
- Set weekly hours and time off.
- Take a booking online or at the desk. The booking stores a service snapshot, segments, and a schedule revision.
- Publish consultation forms. Clients sign. Staff review before service.
- Check in, work each step, then complete or mark no-show.
- Check out: services, retail, discount, tip, package or membership credit. This writes the sale, payment, stock movements, commissions, tip allocations, and loyalty points.
- Refund, void, or partly refund a sale. Correct commission and tips.
- 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:
manualandstripe, infeatures/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_KEYandSTRIPE_WEBHOOK_SECRET. The webhook route takes a provider code likepp_stripe, checks the signature, logs each event once, rejects amount or currency mismatches, and handlespayment_intent.succeeded,payment_intent.payment_failed,payment_intent.canceled,charge.refunded, andrefund.updated. - Email: booking confirmations, gift-card emails, and password resets are sent with SMTP only when
SMTP_HOSTandSMTP_FROMare 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/imagesdo not need it. - Dashboard assistant:
/api/completionuses OpenRouter. It needs a signed-in user withcanAccessDashboard, plusOPENROUTER_API_KEYandOPENROUTER_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.