Salonyar — salon booking with no app to install
A multi-tenant booking system for hair salons: each salon gets a public booking link that opens in any browser, plus a Bale mini app, a salon and stylist panel, and a platform console.
Pilot-ready after 12 phases: unit and e2e tests, a performance budget the build enforces (about 88 KB of gzipped JavaScript on first load), and the main queries measured on about 4.4M bookings.
The problem
Salons take bookings by phone or in Instagram DMs, and the result is double bookings, forgotten appointments and empty chairs. Off-the-shelf booking tools assume two things that do not hold in Iran: that a customer will install an app and sign up for one haircut, and that foreign SMS and push services reach the phone. So a booking has to start from a link, finish in under a minute, and rest on Bale and domestic SMS.
A NestJS backend on PostgreSQL and Prisma, a BullMQ queue on Redis, and one React front end that is both the web page and the Bale mini app.
One link per salon. salonyar.ir/<slug> renders the salon — name, address,
hours, services and prices — on the server (nginx SSI with a 30-second cache),
so a link shared on WhatsApp or Instagram previews the salon rather than an
empty page.
One booking core, two doors. Web and Bale go through the same flow: "any stylist" is the default and the nearest free times come first. A customer browses without signing in and is verified by a one-time code only at the moment of booking. Inside Bale the number is confirmed through Bale's own dialog, with nothing to type, and the web and Bale accounts become one identity.
The database refuses a double booking, not the code. Create, cancel, reschedule, confirm and decline, with a cancellation window per stylist. Two simultaneous bookings for the same slot are stopped by a PostgreSQL exclusion constraint, and a concurrency test checks exactly that.
A mobile-first panel for the salon. A calendar of every booking, phone bookings entered by hand, and a printable link and QR. Salons sign themselves up, and a wizard sets out default services, stylists and hours, then hands over the web link, the QR, the Bale link and a line for the Instagram bio. Plans come with a trial, a stylist limit per plan, and remaining days converted when the plan changes.
Against no-shows: reminders in Bale with SMS as the fallback, and a deposit through Bale's wallet. Notifications leave through BullMQ with an outbox; login codes come from Bale's Safir service and Kavenegar SMS, rate-limited. The production stack backs itself up daily and alerts to Bale, and the API refuses to start with insecure settings.