← Back to portfolio

Appointment system for barbershops

Booking, store, academy and management panelDemo

Complete system for a barbershop: online appointment booking, store, academy and admin panel. A demo with a generic brand that I built at Qontera, with React and a Node API with SQLite.

This is a demo with fictitious data; payments and WhatsApp notifications are simulated. Access is private; if you'd like to see it, write to me.

The business rules (time slot availability, permissions and reports) are pure functions in a shared folder: the browser uses them to build the interface and the server applies them again before saving. The API is built with Hono and stores everything in SQLite, and the deployment is packaged in Docker to be published under a subpath.

Tools developed

Built for Appointment system for barbershops, ordered from most to least complex.

  1. Booking engine with no overlapping appointments

    • TypeScript
    • Hono
    • SQLite

    What it does

    A pure function calculates a barber's free time slots for a day from their working hours, the service duration, the configured interval and the appointments that already take up time (pending, confirmed, completed and no-show). Besides offering one barber's slots, it can merge everyone's, and when "any" is chosen the server assigns the free barber with the fewest appointments that day.

    When booking, the server does not trust what the browser showed: it recalculates the price and the deposit, normalizes and validates the phone number, and if the client already exists it keeps the stored name. Rescheduling an appointment uses the same rule and ignores the appointment itself.

    What makes it complex

    Two people can pick the same slot at the same time. That is why the whole booking runs inside a SQLite transaction opened with BEGIN IMMEDIATE, which takes the write lock before reading: availability is revalidated with the same rules as the browser and, if another appointment won the slot, the second booking is rejected with a conflict instead of being duplicated.

    Because the rule lives in a single module, the interface and the server cannot disagree about which slot is free. Appointments created from the panel can be in the past, to record history, but still cannot overlap another one.

  2. Role and per-barber permissions

    • TypeScript
    • Hono
    • zod

    What it does

    The owner has full access and can manage team, services, products, courses and settings. Each barber is assigned four permissions: view all appointments, edit them, view clients and view reports. A single function turns the user into their effective permissions, and both the screens and the API use it.

    A barber without permission to view everything is limited to their own schedule: the API filters appointments and reports by their barber and prevents them from creating appointments in another barber's schedule. Contact details are hidden from anyone who cannot view clients.

    What makes it complex

    Hiding a button protects nothing, so every panel route goes through a session check and a permission check before running, and the per-barber filters are applied in the query, not afterwards in the browser. Input data is validated with zod schemas.

    Having a single shared permissions function prevents the screen from showing something the server later rejects, or the other way around.

  3. Login and sessions

    • TypeScript
    • Node.js
    • scrypt

    What it does

    Panel access uses passwords hashed with scrypt and a random salt per user, compared in constant time. The session is stored in a cookie that the browser does not let JavaScript read, with an expiry, and it is closed from the server. Logins and the public client lookup by phone have an attempt limit.

    What makes it complex

    The server does not store the session token: it stores its fingerprint computed with a secret key, so a copy of the database is not enough to reuse a session. Expired sessions are purged when others are started.

  4. Reports dashboard

    • React
    • Recharts
    • TypeScript

    What it does

    The panel shows today's appointments, the month's revenue, average ticket, new clients and no-show rate, along with a revenue chart for the last 30 days, each barber's appointments in the month and the most requested services. The calculations are pure functions with tests, and the charts are drawn with Recharts.

    What makes it complex

    Each indicator has a precise definition: the month's revenue adds up only completed appointments, the average ticket divides by those same appointments and the no-show rate is calculated over past appointments that are not pending, confirmed or cancelled.

    The API delivers the reports only to those with that permission, and apart from the owner each barber sees only the figures for their own schedule.

  5. Store and academy with stock and capacity

    • TypeScript
    • Hono
    • SQLite
    • zustand

    What it does

    The store has category filters and a cart on the browser side. On confirmation, the server builds the order with the catalog prices and not the ones the client sends, and deducts the stock. The academy lets people enroll in courses with limited capacity.

    What makes it complex

    Both the order and the enrollment run in a transaction: the stock or capacity is checked and the changes are saved together, so two simultaneous purchases of the last product do not sell it twice and a full course rejects new enrollments. Repeated products in the cart are added together before being compared with the stock.

  6. Self-regenerating demo

    • TypeScript
    • SQLite
    • Node.js

    What it does

    A generator creates appointments, clients, orders, courses and enrollments with dates calculated from today, so the schedule and the reports always show activity. If the database is empty on startup, it loads itself; in demo mode it is reloaded every day at a configured time and the owner can reset it from the panel.

    What makes it complex

    Resetting deletes and reloads all the tables inside a transaction, so nobody sees the database half-loaded. The reset, both automatic and manual, is only available in demo mode.

Want something like this for your store?

Message me on WhatsApp, tell me what you need and we'll see how I can help.

Let's talk on WhatsApp