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.
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.
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.
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, since 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 the response is a 404, not a 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.
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.
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.
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. The retrieval is genuinely semantic, not keyword search with GPT on top.
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.
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.
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.