Back to case studies

Case study

Trimsmith

A barbershop wanted online booking. We built the booking site, the shop-floor system behind it, and a multi-tenant platform that can be sold to the next salon without touching code.

Trimsmith barbershop interior
Client
Trimsmith - Master Barbers, Kochi
Scope
Product, architecture, build, deployment
Shape
Three applications, one database, one session
Stack
Next.js · Express · PostgreSQL · AWS
Status
Deployed and running

The brief

Booking was the ask. Running the floor was the problem.

Trimsmith runs two shops in Kochi with six barbers between them. Bookings came through phone calls and WhatsApp, written into a paper diary. Walk-ins were told "maybe twenty minutes" by whoever was nearest the door.

A booking form would have solved the smallest part of that. The real difficulty is that a barbershop is a live system: chairs fill and empty, barbers clock in late, someone books online while someone else is already standing at the counter. Anything that only writes appointments into a database and hopes for the best will be wrong within a week.

So we built for the floor first, and let the booking site be a client of it, and designed the whole thing multi-tenant from the first schema, because the second salon is where a project like this becomes a product.

What we built

Three applications, one source of truth.

Each has a different audience and a different failure mode. None of them holds a database credential except the one that owns the database.

For customers

The booking site

Browse the menu, pick a barber and a time, pay a deposit or settle at the chair. Fifteen screens.

  • Live slot availability per barber
  • Hold then confirm checkout
  • Google and password sign-in
  • Public wait times per shop
For the shop

The operations dashboard

What the front desk and the owner actually work in, all day. Eighteen screens.

  • Walk-in queue and chair assignment
  • Attendance, rosters, payroll hours
  • Takings, refunds, CSV exports
  • Per-salon white-label settings
For everything

The API

Owns the database and every business rule. Seventy-nine endpoints on a single deployable service.

  • Availability, booking, pricing, offers
  • Queue simulation and attendance
  • Payments and refund policy
  • Role and location authorisation

Engineering

Six decisions that shaped the build.

Every one of these came from how a barbershop actually behaves, not from a framework's happy path.

Two people cannot book the same chair

Checking availability and then writing a row is a race, and races are won in production at the worst possible moment. The guarantee belongs in the database, not in application code that can be run twice at once.

A PostgreSQL exclusion constraint makes an overlapping booking for the same barber physically impossible to insert. Checkout runs as a hold, then a confirm, inside serializable transactions, and the constraint sits underneath as the backstop that cannot be bypassed by any code path, present or future.

EXCLUDE USING gist (staffId WITH =, tsrange(startAt, endAt) WITH &&)

Wait times are a simulation, not an average

"Average wait: 20 minutes" is a number that is wrong for everybody. The person who wants a beard trim and the person who wants colour are not waiting the same length of time.

Every clocked-in barber is modelled as a chair. Occupied chairs count down their remaining service; everyone waiting is dealt onto the earliest chair that frees up. The estimate a customer sees on the website is the same model the shop screen runs on.

This is also why attendance matters operationally rather than just for payroll: nobody clocked in means the model assumes one chair, and the floor looks slower than it is.

Permission and reach are separate questions

A store manager needs to see money, but only their own store’s money. Collapsing "what may you do" and "where may you do it" into a single role list is how a manager at one shop ends up reading another shop’s takings.

A role grants capabilities. A separate set of records pins a person to specific locations. Both are checked on every request, server-side. An out-of-scope request is refused outright rather than quietly returning an empty list, a silent empty result hides a bug until it is a breach.

The second salon is configuration, not a fork

Multi-tenancy retrofitted later is a rewrite. Every owned record has carried a tenant from the first migration, and every query is scoped on the server.

Branding, currency, timezone, booking policy and queue rules live in per-salon settings. Onboarding a whole new salon happens in one transaction from an admin screen, with no engineer involved. A second tenant runs in the same deployment today, which is how we know the path works.

Money in paise, time in the shop’s own zone

No floating-point number touches a price. Every amount is an integer in minor units, converted only at the edge where it is displayed. Rounding errors in a deposit are not a rounding error to the customer.

Time is stored in UTC, but every location carries its own IANA timezone and all day-boundary maths runs through it. "Today’s takings" means today at that shop, never today on the server.

One session across three applications

An owner signed in on the booking site is signed in on the dashboard. A customer who checks out as a guest is signed in from the moment they book, without a second form.

Sessions are minted by the API and set as a cookie by whichever app the browser is actually talking to, so the browser only ever speaks to one origin, and no cross-site cookie configuration exists to be got wrong. Passwords are stored as scrypt hashes; an email that already has a Google account cannot be claimed with a password.

Architecture

How the shape arrived where it did.

The system did not start with three applications. It arrived there for a specific reason, in this order.

01

Two applications, one database

The booking site and the dashboard were separate Next.js apps sharing a single PostgreSQL database and one session. Fast to build, and correct while everything ran on one machine.

02

Hosting forced the question

Server-rendered hosting has no private network route to a database and no fixed addresses to allow-list. Deploying as-is would have meant opening PostgreSQL to the public internet, with customer records and payment rows behind it.

03

The domain logic became a service

Availability, booking, pricing, offers, payments, queue, attendance and access control moved into a standalone Express API that owns the database outright. It runs on a server we control, behind TLS.

04

Both front ends were cut over

Every screen and every action now reads and writes over HTTPS. Neither Next.js application has a database client, a schema, or a session secret any more.

05

The database port stayed shut

PostgreSQL listens on localhost only. Nothing but the API can reach it, from anywhere. The hosting constraint that started this ended up producing the better architecture.

By the numbers

What that adds up to.

3Applications
79API endpoints
43Database tables
33Screens
24.2kLines of TypeScript
56Automated tests

The stack

Chosen to be boring where it matters.

Nothing here is exotic. The interesting decisions are in the schema and the domain model, which is where they belong.

Application

TypeScriptNext.js (App Router)React Server ComponentsExpress 5PrismaZodTailwindVitest

Infrastructure

PostgreSQLGiST exclusion constraintsAWS EC2AWS AmplifynginxsystemdLet's EncryptCloudflare DNS

In short

A booking form takes an afternoon. A system that still tells the truth when the shop is full, a barber is late, and someone walks in off the street, that is the actual job.
KraavonProduct engineeringTrimsmith · Kochi2026