Skip to content
georgepuma.dev

projectscleo-spa

Mini-case · Direct client · in production

Cleo Spa

Inventory, public catalog and bookings for a beauty salon in Arequipa. The tool the staff uses every day — not a showcase.

problem
A salon needs to know what stock it has, who moved it and what can be booked, without any of that depending on someone's memory.
role
Direct client: database, operations panel and public catalog. In production.
scope
Immutable stock ledger with FEFO batches · default-deny RLS on 16 tables · three operational roles (owner, cashier, stylist).
result

insert-only ledger

corrections = adjustments

3 roles

permissions in DB and app

Next.js 16 · React 19 · TypeScript · Tailwind v4 · Supabase · Zod · Vitest

The product, unretouched

Inventory movements table: each row carries type, product and variant, signed quantity, location, date and author.
Movements: each row is a ledger entry. Receipt, consumption, sale or shrinkage, always with a signed quantity and an author.
Users screen: invitation form with a role selector and three team accounts, each with its role and a button to deactivate it.
Users: the three operational roles. Invited by email and deactivated — never deleted.
Catalog table: product name, brand and line, number of variants and status.
Catalog: brand, line and variants — the base the stock moves on.

Three decisions that hold up the rest

  1. Stock isn't edited, it's recorded

    Every movement goes in as a new row with a signed quantity, location, date and author. A correction does not rewrite the past: it goes in as one more adjustment. The balance of any product is the sum of its ledger, so the question “who moved this and when” always has an answer.

  2. FEFO batches, and the warning before expiry

    Stock is tracked by batch and outgoing movements consume the one that expires first. The panel opens with two alerts — below minimum and batches about to expire — because expired stock gets found late or not at all.

  3. Three roles, the same boundary in two layers

    Owner, cashier and stylist. Permissions live in the database with default-deny RLS on 16 tables, and the application repeats that boundary: the interface does not offer what the database is going to reject. Having the rule at the bottom is what makes it hold even when the UI gets it wrong.

What it left behind

This is where the starter kit came from that took Ronatello to production in 12 days: the same skeleton of roles, RLS and CI, already argued through once.

The migrations are versioned and idempotent, but they're still applied with a tool outside the repository: a clean environment can't be spun up from scratch. It's the first thing I'd close before the project changes hands.