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.
The problem
The appointment ends and the salon loses track of the client.
- Nobody notices a client is gone until the calendar empties out.
- Birthdays go by and the message never goes out.
- This month's promotion goes to everyone, or to no one.
- The client list lives in a spreadsheet full of phone numbers typed any old way.
What it does
Who's missing, at the right time
Each service has its own return window, 45 days by default. The shortest window from the last visit wins, so coloring and haircuts are each followed up on their own schedule.
Alert bell
Who is past their window and whose birthday is today or tomorrow. The alert clears itself when the client comes back.
Ready-made WhatsApp message
Lists of missing clients, birthdays and promotions, each item with a button that opens WhatsApp with the salon's message. A person at the salon always hits send.
Team roles
Admin, professional and assistant. The assistant only sees what they need, and the block applies to the menu, the route and the API.
Courtesy rotation
The screen shows who gets the courtesy slot today and for the next four weeks. The schedule is calculated; only the exceptions are stored.
Painless spreadsheet import
Takes the salon's spreadsheet and form CSV exports, fixes hand-typed phones and dates, and can be re-imported without wiping what was edited in the CRM.
Under the hood
The part the salon never sees, but that holds everything together.
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.
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.
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.
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.
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.
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.
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.
- Problem
After the session was refreshed, eight places in the code still read the old token, got a 401 and logged the user out.
FixOne function to get the token, always preferring the refreshed one.
- Problem
Migrations had never run on deploy: production was stuck on an old database version and creating a new salon broke.
FixMigrations became an automatic step before every deploy.
- Problem
People stuck on the password change screen, which asked for the current password without showing the field.
FixThe password state now comes from the database, not from stale data saved at login.
- Problem
Good passwords rejected, with an English message, just for looking like the email.
FixCustom validators, in Portuguese, that only reject what is actually the same.
- Problem
Missing clients meant 45 days for everything: coloring was followed up too late and haircuts too early.
FixA window per service, with the shortest from the last visit winning.
By the numbers
- 45 daysdefault return window, adjustable per service
- 400+automated tests on the back end and front end
- 3 roleson the team, each seeing only what they need
- 2 in 1salon and store on the same code
- 30 dayswindow on the alert bell
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