← Home
IN PRODUCTION November 2025 – Present (ref. 29/07/2026, ~8 months)

Barber Cashflow

Own product — designed, built, deployed and maintained entirely by Paulo.

NUMBERS
R$245K in transaction volume processed by the system 25/11/2025 – 29/07/2026 · ~8 months
~7,000 sales recorded in the period
5 barbers operating daily
+4,000 lines of automated tests
STACK
Django Django REST Framework JWT PostgreSQL Docker Nginx Cloudflare PWA OpenAI Embeddings (text-embedding-3-small) FAISS
THE PROBLEM

The barbershop tracked appointments, sales, commissions and cash flow manually and across separate tools. Without a consolidated view, closing the day meant reconciling notes from different sources. The original brief was a scheduling system.

Partway through development, the scope pivoted: the architecture could support more than one barbershop. The system became a SaaS. That decision brought, all at once, data isolation between tenants, LGPD compliance responsibility (Brazil's data protection law), and the need to ship updates without disrupting an operation already running with a real paying client.

The system now records sales by payment method (PIX, card, cash), calculates per-barber commission per sale, manages inventory and closes the daily till with automatic reconciliation. It runs as a PWA — installable on the barber's phone, with partial offline support.

ARCHITECTURE
Multi-tenant isolation: request → middleware → tenant filter → PostgreSQL

Django + DRF serving the REST API with JWT authentication; PostgreSQL as the shared tenant database; tenant isolation by tenant_id resolved via TenantMiddleware on each request; Nginx as reverse proxy; Docker for reproducible builds; Cloudflare for SSL and CDN. The RAG layer runs separately: system data is chunked, converted to embeddings via the OpenAI API (text-embedding-3-small) and indexed with FAISS. PWA with service worker for partial offline use.

DECISIONS
DECISION

Multi-tenancy: shared-db with tenant_id

Problem
Multiple barbershops need complete data isolation. A barber from one shop must never see — or accidentally access — another shop's data.
Alternatives
Separate database per tenant: maximum isolation, but high operational cost and migrations across N databases for every schema change. Separate PostgreSQL schema per tenant: multiplied migrations and complex connection routing.
Choice
Shared database with tenant_id on each relevant table. TenantMiddleware resolves the current tenant from three sources in priority order: subdomain → HTTP header → query parameter. Tenant filtering is mandatory on every exposed queryset.
Result
One paying tenant in production. The architecture supports multiple barbershops on the same Django instance and the same PostgreSQL database, and adding a new tenant requires no redeploy and no new database instance — isolation between them is enforced by mandatory queryset filtering and proven by tests, not by operating at scale.
DECISION

Isolation tests return 404, not 403

Problem
The test suite must prove that one tenant cannot access another's data. The error response matters as much as the access denial itself.
Alternatives
403 (Forbidden): semantically correct — the resource exists but access is denied. 404 (Not Found): does not reveal whether the resource exists at all.
Choice
404. The view treats another tenant's resource as non-existent for the requesting tenant. Isolation tests actively attempt to access another tenant's data and verify they receive 404 — not 403.
Result
Isolation validated by automated tests. An attacker with valid credentials for one tenant cannot even confirm the existence of resources belonging to other tenants.
DECISION

transaction.atomic on sale cancellation

Problem
Cancelling a sale touches three domains simultaneously: inventory (product return), barber commission (proportional reversal) and till (cancellation entry). An exception mid-operation would leave all three in mutually inconsistent states.
Choice
transaction.atomic() wraps the entire cancellation operation. If any step fails — Python exception, database error or constraint violation — PostgreSQL rolls back all changes automatically.
Result
Atomicity guaranteed at the database level. Business logic needs no manual rollback implementation. In 8 months of operation: zero financial inconsistency from partial cancellation.
DECISION

Soft delete to preserve financial history

Problem
Financial records require complete traceability. Physically deleting a sale would erase the accounting trail and break reconciliation for already-closed periods.
Choice
Soft delete with is_deleted field and SoftDeleteManager that automatically filters deleted records in all standard queries. Audit queries and reports use .all_objects for unrestricted access to full history.
Result
Users cancel records without losing history. Reports for past periods remain intact even after subsequent cancellations.
DECISION

Full RAG pipeline for the internal assistant

Problem
Barbers and the owner need to query system data in natural language — daily balance, weekly commission, low-stock products. Keyword search does not understand natural question variations or financial context.
Choice
Complete RAG pipeline: semantic chunking of system data → embeddings with OpenAI text-embedding-3-small → FAISS vector index for similarity search → relevant context injected into the response prompt.
Result
Users ask in natural language and receive contextualised answers with real system data, without navigating menus. Not keyword search with GPT on top — real semantic retrieval.
WHAT BROKE AND HOW I FIXED IT
● INCIDENT

N+1 on the sales listing

Symptom
Listing 20 sales was noticeably slow. The client flagged the slowness weeks after the system went live — there was no query-count monitoring configured at launch.
Investigation
Django Debug Toolbar showed 61 queries to render 20 records: 1 query to list the sales + 20 for the barber on each sale + 20 for the service + 20 for the associated appointment.
Cause
FK relations (barber, service, appointment) were not being pre-fetched in the view's queryset. The ORM issued a separate query for each object the moment the template accessed each FK.
Fix
select_related('barbeiro', 'servico') for the direct FKs — becomes a single JOIN. prefetch_related('agendamento') for the reverse relation — becomes a separate IN query. Result: 61 queries → 3 queries. Same listing, no template changes.
RESULT

R$245K in transaction volume processed by the system between 25/11/2025 and 29/07/2026 (~8 months), with ~7,000 sales recorded and 5 barbers operating daily. Payment method breakdown: PIX R$143,335 · Card R$72,210 · Cash R$30,150.

No isolation incidents in 8 months with the tenant in production. Cross-tenant access is covered by tests that actively attempt to read another tenant's data and verify the 404 response. The +4,000-line automated test suite covers multi-tenant isolation, financial transaction atomicity, soft delete and the RAG interface.

SCREENSHOTS
LEARNING

This is when I stopped seeing software as just code and started seeing it as a product that must keep working while it evolves. The pivot from a single-tenant system to a SaaS — with a real client already using it — made concrete what had been theoretical: data isolation, transactional consistency and tests as living documentation of expected behaviour.