The MaidMatch admin dashboard, with revenue, total bookings, new requests, completed jobs and the latest bookings with their status

The problem

A home-cleaning service needs two things: customers who can book without a phone call, and a team that can plan the jobs, send the right person and follow payments. Without rules in one place, the same person gets two jobs at once, a cancelled job comes back, and the price shown to the customer differs from the price saved.

The approach

Three projects work together: a Next.js booking site, a Next.js admin dashboard and a NestJS API with PostgreSQL. The API owns every business rule, and both websites only display what it decides.

The booking site sends the booking to the API, which checks every field, computes the price from the hourly rate in the settings and saves the booking. It then pushes a notification to the signed-in admins. The dashboard reads and changes bookings only through API routes protected by a JWT, and every change is checked and written to an audit log.

Who uses it

Customers
Book a cleaning on the public site, with a price before they confirm.
The team
Plan the jobs, assign staff, follow requests and payments in the admin dashboard.

What it does

  • A four-step booking form with a price estimate from the API, for one-time, weekly or every-two-weeks cleanings
  • Every date handled in Paris time, so a booking made from abroad keeps the right hour
  • A dashboard with revenue, bookings, new requests, completed jobs, live notifications and a search that ignores accents
  • A fixed status order: requested, confirmed, assigned, completed, cancelled or refused
  • Staff assignment that refuses people on leave and overlapping jobs
  • Recurring bookings: completing one creates the next at the same Paris time
  • Internal notes, a weekly planning and a staff catalogue with filters
  • A staff profile with a printable sheet, and an audit log of every change
  • A legal notice and a privacy page written for a demonstration

Where the engineering is

One price, computed in one place

At first the site and the API each had a copy of the price formula, so the price shown to a customer could differ from the price saved. Now the API gives the estimate, from the hourly rate in the settings, and a unit test proves that the estimate equals the price that is saved.

The last step of the booking form, with a summary of the cleaning, the date and time, the address and a price estimate of 90 euros, Open the full-size image (opens in a new tab)
The summary, with the price from the API.

Nobody booked twice at the same time

The first version had a single busy flag per person, which blocked them for every day. It was replaced by a check of overlapping time slots, so a person can be assigned to any job that does not overlap another one.

Assignment also refuses people who are on leave, and a booking can only move through the fixed status order, so a cancelled job cannot come back.

A booking request with a staff member assigned, the property details, the planned date and the client's details, Open the full-size image (opens in a new tab)
A request with its assigned staff member.

A weekly job that stays at 9:00

A weekly 09:00 job must stay at 09:00 in Paris after the switch to winter time. Adding 7 times 24 hours is not enough, so the next date is computed in Paris clock time, and completing a recurring job creates the next one at the same hour.

Every date is handled in Paris time, so a booking made from abroad keeps the right hour. Recurring bookings across clock changes are covered by unit tests.

The schedule step of the booking form, with once, weekly and every two weeks choices, a date picker and a time slot, Open the full-size image (opens in a new tab)
Once, weekly or every two weeks.

Live bookings behind a token

New bookings reach the signed-in admins through Socket.io, and that live channel also needs a token. Every API route needs a JWT unless it is marked public, which 5 of the 26 endpoints are.

Sign-in, bookings and price estimates have their own rate limits on top of a global one, and text fields are trimmed and cleaned of HTML before they are saved.

The dashboard with the notifications panel open, listing new cleaning requests, Open the full-size image (opens in a new tab)
New requests arrive live.

Screens

How it's built

LayerTechnology
Booking siteNext.js 16, React 19, Tailwind CSS 4 and Zustand, to keep the form between steps
AdminNext.js 16 and React 19, with a Socket.io client for new bookings
APINestJS 11 with JWT sign-in, rate limits, helmet and class-validator
DatabasePostgreSQL with Prisma 7: 6 models and 12 migrations, including data migrations
TestsJest for the API, with lint and type checks on the three projects
Architecture diagram: the public booking site and the admin dashboard both call the REST API, which uses PostgreSQL, Open the full-size image (opens in a new tab)
Two websites, one API that owns the rules, one database.

Testing

58 unit tests in 12 files cover the business rules: the status order, overlapping jobs, recurring bookings across clock changes and a price estimate equal to the saved price. The three projects have no lint problems, and every one of them type-checks and builds.

Results

  • 3 repositories with 140 source files and 9,367 lines of code.
  • 5 pages on the booking site and 10 in the admin.
  • 26 API endpoints, 5 of them public, over 6 database models.
  • 58 unit tests and 78 commits.

What I would build next

  • Send real emails and text messages through a provider, instead of the simulated ones.
  • Move the admin session from browser storage to a secure, http-only cookie.
  • End-to-end tests for the two websites, and a CI pipeline.
  • Let customers cancel or move a booking themselves.

Built with

  • Next.js
  • React
  • TypeScript
  • Tailwind CSS
  • Zustand
  • NestJS
  • Prisma
  • PostgreSQL
  • Socket.io

56 results