All writing

Writing

Building a Pet Boarding CRM: Lessons from Apollon

By Aggelos Konstantilieris·April 18, 2026·3 min read·Updated October 28, 2026

I built Apollon CRM as a freelance project over 2.5 years, from 2021 to 2024, and I still maintain it. A pet boarding business tracked owners, pets and reservations in spreadsheets, and every busy weekend showed the gaps. Before writing code, I had to name the domain.

Modelling the domain

A boarding business has five things that keep referring to each other: clients, pets, bookings, rooms and money. A client owns many pets. A pet appears in many bookings. A booking takes one room for a date range and produces one invoice.

I did not flatten this into one "reservation" table. Each concept got its own model:

  • Clients hold contact details and a billing profile.
  • Pets belong to a client and hold medical notes, diet and temperament flags.
  • Bookings join a pet to a room for a check-in and check-out window.
  • Rooms track capacity and which species they suit.
  • Invoices hold the money side of every booking.

If the relationships are wrong, every screen built on them is confusing too.

Why bookings are the hub

Almost every question in the app starts from a booking. "Who is staying tonight?" is a booking query. So is "Is room 4 free next Tuesday?". "What does this client owe?" goes from client to bookings to invoices.

Bookings are read constantly, so I denormalized a little. Each booking stores the pet's name and the client's name, so the daily roster renders without three joins. The write path keeps those copies in sync.

A simplified availability check looks like this:

type Booking = {
  roomId: string;
  checkIn: Date;
  checkOut: Date;
};
 
function roomIsFree(existing: Booking[], roomId: string, from: Date, to: Date) {
  return !existing.some(
    (b) =>
      b.roomId === roomId &&
      from < b.checkOut &&
      to > b.checkIn // standard interval overlap
  );
}

Money without a ledger

The finance module nearly sank the project. My first plan was a full double-entry ledger, which was far too much for a business with a dozen rooms. I settled on three steps:

  1. Confirming a booking creates a draft invoice.
  2. Line items for boarding, grooming and extras attach to that invoice.
  3. Payments reduce the balance until it reaches zero.

That matches how the staff already think about money. The table they open every morning is a status board:

PetRoomCheck-outBalance
Luna32026-04-20€0
Biscuit12026-04-21€45
Maple42026-04-22€120

Most of the work was in the model. I denormalized only where reads needed it, and the money follows the steps staff already use. The dashboards, notifications and Next.js front end sit on top of that model.

The Apollon case study shows the CRM and my role.