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 — 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.
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. Not keyword search with GPT on top — real semantic retrieval.
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.