Skip to content
Daniel Batista
Back to the portfolio

My own product · in production

Salon CRM: bringing missing clients back to the calendar

A white-label CRM for beauty salons, with a retail version running on the same code. The salon sees who stopped coming, whose birthday it is and who fits this month's promotion, and sends a ready-made WhatsApp message. I built it from the database to the screen; here is how it works on the inside.

Recreation with fictional data

The problem

The appointment ends and the salon loses track of the client.

What it does

Under the hood

The part the salon never sees, but that holds everything together.

  1. One salon can't see another

    Every query requires the salon to be set: without it, the query stops instead of returning everything. The salon comes from the address and only counts if the login belongs to it, and another salon's record returns 404, as if it didn't exist.

  2. Each salon at its own address

    One subdomain per salon, with its logo and colors. Text contrast is calculated from luminance, so no color choice makes the screen unreadable.

  3. Session kept away from JavaScript

    The token lives in an HttpOnly cookie and is refreshed silently. Switching subdomains doesn't ask for a new login.

  4. Spreadsheets off the request

    Imports go to a Dramatiq and Redis queue, read Excel files in chunks and are retried if the server dies halfway.

  5. Alerts without cron

    The platform has no scheduler: the day's alerts are created on the first look at the bell, and a database lock makes sure only one request creates them.

  6. Salon and store on the same code

    The retail version, with catalog, stock, per-item sales and repurchase, runs on the same back end and database, split by platform. A bug fixed on one side is fixed on both.

  7. Deploys with no forgotten step

    Migrations run on their own before every deploy, and logos and photos live in storage with no egress fees.

Stack

  • Next.js
  • Django
  • Django Ninja
  • Dramatiq
  • PostgreSQL
  • Redis
  • Cloudflare R2

What went wrong and how I fixed it

Not everything worked the first time. These were the stumbles that taught me the most.

By the numbers

Want something like this for your business?

A salon, a store or any business that lives on returning clients. Tell me the problem and I'll tell you what makes sense.

Message me on WhatsApp