WellRox started from something anyone who has booked a haircut has noticed: you still book by phone, during business hours, from someone whose hands are in someone else's hair.
I built a platform where people find beauty and wellness businesses — hair salons, barbershops, nail studios, wellness studios — and book without calling. On the other side, the owner manages services, staff, schedules and appointments from a backoffice.
Three audiences, one application
That was the first structural decision. The same application serves:
- The visitor — search, categories, business page, booking
- The owner — dashboard, services, staff, schedules, appointments, photos, analytics, subscription
- Me — platform administration
Three separate applications would be cleaner on paper and three times the maintenance. One Symfony app with separate firewalls per area does the same job.
The hard part is not the one you expect
Everyone assumes the hard part of a booking system is the calendar. It is not. It is availability.
To answer "what times are free on Thursday", you have to cross-reference:
- The business hours for that day
- The specific staff member's hours, which may differ
- The duration of the service — a trim does not take as long as a colour
- Existing appointments, and how they overlap
- Breaks
And then comes the question that breaks everything: "which days this month have at least one free slot?" Because that means running the calculation above thirty times, before the user even sees the calendar.
AvailabilityService ended up at 165 lines. Not because it is clever — because every line is a case that showed up in real use.
Greek URLs, on purpose
The routes are transliterated Greek:
/epixeirhsh/{slug}— business/epixeirhsh/{slug}/rantevou— appointment/kathgoria/{slug}— category/anazhthsh— search
It was a deliberate choice for a Greek market: people search for «ραντεβού κομμωτήριο», not "appointment hair salon". The URL is one of the few places where the keyword still counts, and transliteration carries the meaning without the problems of URL-encoded Greek characters.
I would do it again. Though I admit anazhthsh is not beautiful.
The stack
Symfony 7.2 on PHP 8.3, PostgreSQL 15, Doctrine ORM 3, Twig, all in Docker. Nothing exotic, and that was the point.
I picked PostgreSQL over MySQL for one specific reason: time queries. When you are looking for overlapping appointments and ranges, Postgres types and operators solve in the database what you would otherwise hand-write in PHP.
The model is small and tight: Business, Staff, BusinessService, Appointment, Category, Review, Plan, Subscription. An Appointment points at a business, a service and a staff member — three relations that define everything you need to know.
What I learned
The hardest part was not the code. It was the two-sided problem: you need businesses to attract people, and people to attract businesses. No framework solves that.
Businesses do not want another tool. They want fewer phone calls. Every field you ask them to fill in is a reason not to come back.
Availability is the product, not a detail. If the calendar shows a slot that is actually taken, you lost the customer — and the business.
What I would change
I would cache availability from the start instead of leaving it for later. The calculation is deterministic: for a given business, day and service, the result only changes when an appointment or a schedule changes. It is almost designed for event-based cache invalidation, and I did not take advantage of that.
And I would start with one city instead of all of Greece. Density matters more than coverage in platforms like this — twenty salons in one neighbourhood beat two hundred scattered across the country.
wellrox.gr is live if you want to look.
