Openfront Barbershop
Services, barbers and chairs, booking, walk-in queue, POS, customers, payroll, inventory, and reports for one shop or several.
Source · Live site · Catalog
Openfront Barbershop is a self-hosted Next.js and Keystone app for a barbershop. Customers browse services and barbers, book a time, join the walk-in line, and see their visits and balances. Staff run the booking board, walk-in queue, POS checkout, customer records, packages and memberships, barber schedules, payroll, retail stock, policies, and reports from /dashboard/platform.
Start with Getting started and Deployment. Each dashboard area has its own page, starting with the booking board. Task guides are under How-to guides. The API is in the API reference.
Online payment is a Stripe Checkout for booking deposits, available when STRIPE_SECRET_KEY is set and Stripe is set up on the Payments page. Counter checkout takes cash, card (recorded by staff), or a prepaid package visit. Retail products, packages, and memberships are sold at the counter.
Tenancy
Shop is the tenant. Almost every merchant record has a shop relation. Reads and writes are scoped to the caller's active shop (features/keystone/access.ts).
- A
ShopMembershiplinks aUserto aShopwith a role:owner,manager,front_desk,barber, orviewer. Status movesinvited→active→suspendedorrevoked. Memberships are not deleted, and each shop keeps at least one active owner. - The membership role narrows the user's global
Roleflags inside that shop. For example, abarbermembership allows appointments, queue, customers, and customer notes, but not services, staff, payments, or reports. Aviewercan open the dashboard and view reports. selectActiveShopswitches the active shop. The selector is on the Store settings page.- If a dashboard user has no shop, the platform pages show a shop access panel instead. Instance admins (users with both
canManagePeopleandcanManageRoles) can take over an existing shop withclaimShopOwnership. Everyone else needs an owner or manager to add them. - The storefront shows exactly one shop: the single active shop with
isPublicset. If none or more than one is public, public booking is unavailable.
Data model
The schema has 49 Keystone lists in features/keystone/models. The repository's docs/schema-map.md describes each one.
| Area | Lists |
|---|---|
| Shop and access | Shop, ShopSettings (legacy; superseded by Shop fields), ShopMembership, User, Role |
| Service menu | ServiceCategory, Service, ServicePackage, ServicePackageItem, MembershipPlan |
| Team and time | Barber, BarberChair, BarberSchedule, ScheduleException, StaffShift, PractitionerCredential |
| Bookings | BookingPolicy, Appointment, AppointmentChangeEvent, AppointmentLineItem, NoShowPolicy, NoShowEvent, WalkInQueueEntry, WaitlistEntry, GuestBookingCommand |
| Customers | Customer, CustomerNote, GroomingNote, WaiverForm, CustomerPackage, PackageUsage, MembershipEvent |
| Money | CheckoutTransaction, CashRefund, PaymentProvider, PaymentSession, PaymentReconciliation, PaymentWebhookEvent, Tip, Commission, CommissionPayout |
| Retail | Product, Supplier, InventoryLot, InventoryMovement, ProductSaleLine |
| System | LifecycleOutboxEvent |
Many state-bearing lists cannot be created or updated through generic CRUD. They change only through named commands. Those commands take an idempotency key, write a ledger row where one exists, and record an event in LifecycleOutboxEvent.
Key lifecycles:
- Appointment:
requested→confirmed→checked_in→in_chair→completed.completedis reached only through a paid checkout.cancelledandno_showare terminal. Check-in and seating happen only on the visit's shop day. A no-show can be marked only after the start time plus the policy grace period. - Walk-in:
waiting→texted→in_chair→served, orleft. Seating goes throughseatWalkInQueueEntry, which creates an appointment. Waiting entries from earlier days close asleft. - Waitlist:
waiting→notified→bookedorexpired. - Commission:
projected→approved→paid, orvoided. - CheckoutTransaction:
open→paid→partially_refundedorrefunded, orvoided. - CustomerPackage:
active↔paused→used_up,expired, orcancelled. - PractitionerCredential:
active↔suspended→revokedorexpired. Credentials are only enforced for services markedcredentialRequired. - BookingPolicy: versions move
active→retired. Each booking stores a snapshot of the policy in force.
Named GraphQL operations
These custom operations are defined in features/keystone/mutations/index.ts. Generated Keystone CRUD exists for lists that allow it.
Public and account queries: getPublicStorefrontSettings, getPublicServiceCatalog, getPublicBarbers, getPublicBarberWeek, getPublicProducts, getPublicMembershipOffers, getPublicBookingEligibility, getPublicPaymentOptions, checkBarberAvailability, getPublicAppointment, getPublicQueue, getPublicQueueEntry, getPublicWalkInStatus, accountBookings, accountBalances, exportMyAccountData.
Public and account mutations: createOnlineBooking, recoverGuestBooking, createAccountBooking, recoverAccountBooking, managePublicAppointment (cancel or reschedule, with a fee preview), signPublicWaiver, startDepositCheckout, confirmDepositCheckout, createPublicQueueEntry.
Dashboard projections: platformBooking, platformQueue, platformCustomers, platformPos, platformReports, platformServices, platformStaff, platformOperations, platformTeam, activeShopStorefrontSettings, claimableShops.
Dashboard commands:
| Area | Operations |
|---|---|
| Shop and team | selectActiveShop, claimShopOwnership, addShopMemberByEmail, updateActiveShopStorefrontSettings, runBarbershopOnboarding |
| Appointments | createStaffAppointment, manageStaffAppointment, transitionAppointment, markAppointmentNoShow |
| Walk-ins and waitlist | createStaffQueueEntry, assignWalkInQueueEntry, transitionQueueEntry, seatWalkInQueueEntry, manageWaitlist |
| Checkout and payments | checkoutAppointment, refundCashCheckout, recordProductSale, returnProductSaleLine, ensureDefaultPaymentProvider, initiateAppointmentPaymentSession, captureAppointmentPaymentSession, refundAppointmentPaymentSession, cancelAppointmentPaymentSession, reconcilePaymentSession |
| Customers | addCustomerNote, setCustomerStatus, setCustomerConsent, issueCustomerWaiver, manageMembership (purchase, purchase package, renew, pause, resume, cancel) |
| Staff | manageStaffShift, manageScheduleException, linkBarberAccount, setPractitionerCredentialStatus |
| Payroll and stock | transitionCommission, payApprovedCommissions, receiveInventoryLot, recordInventoryMovement |
Main workflow
- Set up the shop: name, timezone, hours, brand color, and publication on Store settings.
- Build the menu: categories, services (price, duration, deposit, barbers, licensed flag, waiver), packages, and membership plans.
- Add barbers, chairs, and weekly hours. Add shifts, time off, and shop closures.
- Publish a booking policy version and set a no-show policy.
- Customers book online or join the walk-in line. Staff can also book or add walk-ins from the dashboard.
- On the day, staff confirm, check in, and seat. Walk-ins are seated into an appointment.
- POS checkout takes payment (cash, card recorded by staff, or a package visit), records tips and line items, and creates projected commissions.
- Payroll approves and pays commissions per barber. Inventory receives deliveries and records waste and count corrections.
- Reports show revenue, appointments, walk-ins, and barber and service performance for a date range.
Integrations
- Payments: two adapters are registered in
features/integrations/payment.manualis offline (pay in person) and is the default provider (pp_manual).stripeuses Stripe Checkout for booking deposits only, needsSTRIPE_SECRET_KEY, and confirms payment by reading the Checkout Session back from Stripe. Stripe deposits are confirmed by reading the Checkout Session back from Stripe.POST /api/payment-webhooks/[providerCode]serves providers that register a webhook handler.checkoutAppointmentrejects a processor payment session at the counter. - Email: SMTP sends password reset emails, booking and status emails, and reminders about a day before a visit. The reminder sweep runs on ordinary traffic (storefront queue and dashboard projections), at most every five minutes per server. Without
SMTP_HOST,SMTP_USER, andSMTP_PASSWORD, emails are not sent. - Images: service, barber, and product images use S3-compatible storage configured with the
S3_*variables. - AI assistant and MCP:
/api/completionand/api/mcp-transport/[transport]return 403 unlessENABLE_AI_TOOLS=true. The chat also needsOPENROUTER_API_KEYandOPENROUTER_MODEL. The MCP tools are generic Keystone tools (listModels,queryData,searchModels,getFieldsForType,lookupInputType,createData,updateData,deleteData,modelSpecificSearch). They run with the caller's session cookie.