← Barber Cashflow
IN PRODUCTION 2026 – Present · used daily by staff

Açaí Management

Built entirely by Paulo, for a real family business.

NUMBERS
~1h → ~10min daily cash closing
~296 tests running against real PostgreSQL on every push
80% coverage gate — build fails below this
+115 orders · R$4.6K recorded in the first week of the orders module
STACK
Django PostgreSQL (Neon) Render GitHub Actions Ruff pytest rclone Google Drive API
THE PROBLEM

Financial management system used daily by staff at a real family business. The primary user is non-technical: a regression in production is not an abstract bug — it disrupts the working day, the cash closing and inventory control.

The system already had ~296 tests and a daily backup configured. But nothing prevented broken code from going live: Render deployed directly on every git push, without running a single test. The protection existed only on paper.

The daily cash closing took about an hour because it was done manually, reconciling records from different payment methods without a consolidated view. With the till module, the same process now takes ~10 minutes.

ARCHITECTURE
CI/CD pipeline and automated daily backup

Django with CBVs and a layered architecture (View → Service → Selector → Model); PostgreSQL on Neon (serverless, no server to manage); hosted on Render; GitHub Actions orchestrating lint, tests and deployment; daily backup via scheduled GitHub Actions workflow — pg_dump from Neon's direct endpoint → gzip → rclone → Google Drive → rotation of the 15 most recent files. No artefacts persist on the ephemeral runner.

DECISIONS
DECISION

CI/CD pipeline with deployment gate

Problem
Broken code could go live. A push to main triggered an immediate deploy on Render, without running lint or tests. With a non-technical user depending on the system, any visible regression became an incident.
Alternatives
Manual discipline: run tests locally before every push. Works until the day it doesn't. Deploy with toggle: disable auto-deploy and deploy manually — swaps one automation for a more fragile one.
Choice
GitHub Actions pipeline: push/PR → Ruff (lint) → pytest against real Postgres → 80% coverage gate → only with green CI does the deploy job call Render's Deploy Hook. Branch protection on main makes checks mandatory and blocks direct pushes. autoDeploy: false in render.yaml disables push-triggered deploys. Central rule: deployment only happens after green CI.
Result
No deployment happens with a red CI. Main is always in a deployable state. The pipeline is version-controlled in the repository — anyone reading the code understands exactly what must pass before going to production.
DECISION

Real PostgreSQL in CI, not SQLite

Problem
Tests that pass on SQLite can fail on PostgreSQL due to type differences, constraint behaviour, Postgres-specific migrations or SQL SQLite does not support. That kind of bug would only surface in production.
Choice
Ephemeral PostgreSQL service container on the GitHub Actions runner. It starts alongside the test job, passes a health check before tests run, and is destroyed at the end. Mirrors the exact production database (Neon/Postgres).
Result
~296 tests running against real PostgreSQL on every push. No production-only database bugs since the change. The CI validates the same environment the user runs on.
DECISION

Daily backup via GitHub Actions, not server cron

Problem
The original roadmap called for cron + rclone on an always-on server. On Render free that does not exist: the instance hibernates on idle, the filesystem is ephemeral and there is no cron support. Losing financial data without a backup is unacceptable.
Alternatives
Upgrade to a paid Render plan (recurring cost with no technical benefit). Periodic manual backups (human reliability, not system reliability). Separate VPS just for cron (infrastructure to solve a lack of infrastructure).
Choice
Scheduled GitHub Actions workflow (cron: '0 3 * * *'): pg_dump with Neon's direct endpoint (no PgBouncer pooler) → gzip → minimum size validation → rclone to Google Drive → upload validation → rotation keeping the 15 most recent files. Nothing persists on the ephemeral runner. Validated in production on 17/07/2026.
Result
Automated daily backup with no extra server to maintain. Automatic rotation of 15 files. Every GitHub Actions run is logged and auditable — no dependency on additional infrastructure.
DECISION

Layered architecture documented before writing code

Problem
Without clear layer boundaries, business logic migrates into views, tests become coupled to the HTTP layer, and any logic change requires edits in multiple places.
Choice
View → Service (write) / Selector (read) → Model. Views contain no business logic. Services encapsulate write logic with transactions. Selectors isolate complex queries. Naming rules, pagination, indexes and validation conventions written in PROJECT_RULES.md before the first model.
Result
Service tests with no dependency on HTTP request/response. Query refactoring without touching views. New modules follow the same pattern without additional instruction.
WHAT BROKE AND HOW I FIXED IT
● INCIDENT

pg_dump 16 incompatible with Neon PostgreSQL 18

Symptom
The backup workflow failed with a version incompatibility error: the Ubuntu runner came with pg_dump 16, but Neon ran PostgreSQL 18. Dumping from a lower version client to a higher version server is not supported.
Cause
GitHub Actions uses the pg_dump version pre-installed on the runner, which does not automatically match the target database version.
Fix
Install pg_dump 18 via the PGDG repository and add the binary to the front of PATH with echo 'path' >> $GITHUB_PATH. The correct version is found first, without conflicting with the pre-installed one.
● INCIDENT

Neon pooler incompatible with pg_dump

Symptom
pg_dump failed with a protocol error when using Neon's default endpoint (with PgBouncer). The connection was established, but the dump never completed.
Cause
PgBouncer (connection pooler) does not support the communication protocol pg_dump uses to perform dumps. pg_dump requires a direct connection to the database.
Fix
Use Neon's direct endpoint (without pooler) in the workflow environment variable. Neon provides both endpoints — the pooled one for the application and the direct one for administrative operations.
● INCIDENT

Multi-line secret rejected by GitHub

Symptom
The rclone configuration string for Google Drive spans multiple lines. When trying to add it via the GitHub interface, the secret was corrupted or rejected.
Fix
Register via CLI: echo 'multi-line content' | gh secret set RCLONE_CONFIG. The pipe preserves line breaks that the GitHub interface does not accept directly.
● INCIDENT

Rotation deleting backup before confirming upload

Symptom
In the first version of the workflow, rotation of old backups ran before validating that the new file had been successfully uploaded to Drive. A silent upload failure would delete the history without a valid new backup.
Fix
Reorder the steps: upload → validate the file on Drive (size and existence) → only then rotate the oldest files. A failure at any validation step stops the workflow before deleting anything.
RESULT

Daily cash closing reduced from ~1 hour to ~10 minutes. In the first week of the orders module: +115 orders and R$4.6K recorded by the system.

~296 automated tests running against real PostgreSQL on every push, with an 80% coverage gate. Automated daily backup validated in production since 17/07/2026, with rotation of 15 files on Google Drive. No deployment with a red CI since the pipeline was implemented.

SCREENSHOTS
LEARNING

Having tests without a deployment gate means having a safety net with holes. CI/CD is not added complexity — it is what turns tests into a real guarantee. And a backup without upload validation is not a backup: it is a ritual that gives a sense of security without the security itself.