Industrial PPE (Personal Protective Equipment) management system. NR-6 and eSocial S-2240 compliant. Single Python process, no web framework. In production at a Brazilian torque-converter remanufacturing plant since September 2025.
Brazilian labor regulation NR-6 requires every employer to (a) issue role-appropriate PPE to every worker, (b) record each delivery with the employee's signature, and (c) preserve that record for 20 years (TST jurisprudence). Without that trail under a Ministry of Labor audit, the company is exposed to uncapped labor-court damages and operational shutdown.
Off-the-shelf options on the Brazilian market are heavyweight ERP add-ons priced for companies the plant does not have. epi.manager is one Python process and one SQLite file on a LAN, sized for the team that uses it.
- PPE catalog with Certificate of Approval (CA), validity windows, manufacturer and lot — CA tracking is required by NR-6.
- Employees with role, registration number and CPF.
- PPE delivery, single item or multi-item batch, with a signed PDF per delivery. The whole batch rolls back if any item is short.
- Pre-configured kits per role (welder, mechanic, electrician).
- Compliance dashboard: CA expiration alerts, NR-6 90-day inspection cycle, low and out-of-stock thresholds, daily and monthly aggregates.
- Audit trail preserved for the 20 years required by NR-6 (soft-delete + JSON snapshot of every deletion).
- Keyboard-first UI: ⌘K command palette,
g+ letter navigation, theme toggle.
- No web framework.
http.server.ThreadingHTTPServerfrom stdlib. Routing is a flatif/elifladder inapp.py:do_GET/do_POST, ~700 lines readable in one sitting. Trade-off discussed below. - Custom multipart parser. Python 3.13 removed
cgi.FieldStorage;MultipartForminapp.py(~60 lines) reimplements it on top ofemail.parserfrom stdlib. No new runtime dependency. - Thread-safe SQLite via
threading.local()connection-per-thread + WAL. Replaces an earlier singleton withcheck_same_thread=False, a documented anti-pattern underThreadingHTTPServer(write-up inCRITICA.md). - Atomic stock decrement under concurrent withdrawal:
UPDATE stock SET qty = qty - ? WHERE qty >= ?plus arowcount == 0check inside the transaction. TOCTOU coverage intests/test_assignments.pyuses a monkey-patched saboteur to force a depletion between check and update. - CSP with per-request nonce on inline
<script>.script-src-attr 'unsafe-inline'(CSP3) is a known compromise for legacyonclick=attributes and is scheduled for removal once the inline JS is extracted. - CSRF double-submit cookie, ~30 lines,
secrets.compare_digest. - Password hashing: PBKDF2-SHA256, 200k iterations, 16-byte per-user salt. Sessions persisted in SQLite (survive restart, not bound to a single worker).
- CPF / LGPD: normalized to 11 digits at the service boundary,
CHECK (cpf GLOB '[0-9]...')at the schema,CpfMaskFilterrewriting11144477735→111.***.***-35in every log record before it leaves the process. - Soft-delete with snapshot.
products.deleted_at+ JSON snapshot indeletion_audit. Hard-delete would violate the 20-year retention requirement. - Defense-in-depth schema.
CHECKconstraints on every table (enums, qty > 0, cost ≥ 0, ISO date format, CPF format),ON DELETE RESTRICT/SET NULL/CASCADEper relationship intent. Direct writes via the SQLite CLI still respect the invariants.
flowchart TB
Browser[Browser]
Server[ThreadingHTTPServer<br/>:8140]
Handler[EpiRequestHandler<br/>do_GET / do_POST<br/>~1.1k lines]
Auth[core/auth.py<br/>session + CSRF + PBKDF2]
Multipart[MultipartForm<br/>email.parser<br/>custom]
Services{{services/}}
DB[(SQLite + WAL<br/>12 tables)]
PDF[pdf/receipts.py<br/>ReportLab]
Audit[(deletion_audit<br/>20-yr trail)]
Logs[CpfMaskFilter<br/>LGPD-safe logs]
Browser -->|HTTP on LAN; TLS planned| Server
Server --> Handler
Handler --> Auth
Handler --> Multipart
Handler --> Services
Services --> DB
Services --> Audit
Services -->|out of tx| PDF
Auth --> DB
Services -.-> Logs
subgraph "services/"
S1[products]
S2[employees]
S3[stock]
S4[assignments]
S5[kits]
S6[compliance]
S7[audit]
S8[dashboard]
end
A request flow for the PPE delivery hot path:
POST /assign
→ require_auth() — session lookup in SQLite
→ validate_csrf() — double-submit cookie check
→ svc_assignments.assign_epi(conn, product, employee, qty)
→ normalize_cpf(employee_cpf) # 111.444.777-35 → 11144477735
→ BEGIN TRANSACTION
→ check employee exists
→ UPDATE stock SET qty = qty - ? WHERE qty >= ?
if rowcount == 0: raise (rollback)
→ INSERT INTO assignments
→ UPDATE pdf_filename
→ COMMIT
→ generate_pdf(...) outside transaction
→ 303 redirect → /assignments?msg=Delivery+recorded
Flask or FastAPI would have saved ~300 lines and added one dependency. Stdlib won because (a) the deploy target is one industrial PC with cautious IT, (b) the request handler is a one-afternoon read for the next engineer, and (c) framework abstractions cost more than they save at this size. The trade-off is real: no worker pool, no graceful reload, no battle-tested middleware. Migration to Flask + Jinja2 is documented in CRITICA.md, gated on >5 concurrent users or non-LAN exposure.
Python 3.13 removed cgi.FieldStorage. Most projects pin <3.13 or pull in python-multipart; this one reimplemented it on top of email.parser from stdlib in ~60 lines. No new dependency, and the upload path stayed working through the Python upgrade.
No build step, no JS framework, no SPA hydration. The plant has 50 Mbps shared internet and a Windows PC running the server. Server-rendered pages stay under 50 KB and avoid a class of bugs (client/server state desync). Cost: every UI change is a Python deploy.
authlib, flask-login and friends would have been faster to write, but each is a dependency to track for CVEs over the product's 20-year lifetime. PBKDF2 is in stdlib, sessions are ~30 lines of SQLite and CSRF double-submit is another ~30. The whole auth surface lives in one file and reviews end-to-end.
| Layer | Choice | Notes |
|---|---|---|
| Language | Python 3.12+ | stdlib-first |
| HTTP | http.server.ThreadingHTTPServer |
no Flask / FastAPI |
| Persistence | sqlite3 + WAL + threading.local |
thread-safe, single file |
| Templates | f-string in Python | server-rendered, no build step |
| ReportLab | only third-party runtime dep | |
| Auth | PBKDF2-SHA256, sessions in SQLite, CSRF double-submit | stdlib only |
| Tests | pytest + requests | 36 tests, in-process server fixture |
| Lint | ruff | strict config in pyproject.toml |
| CSS | hand-rolled tokens, ~1.2k lines | no build step, dark/light themes |
epi_system/
├── app.py # HTTP routing, multipart parser, server bootstrap (~1.1k LOC)
├── run.py # entrypoint
├── pyproject.toml # deps + ruff + pytest config
│
├── core/
│ ├── auth.py # login, sessions in SQLite, CSRF, PBKDF2 hashing
│ ├── db.py # threading.local conn, schema, migrations v1-v4
│ ├── log_filters.py # CpfMaskFilter (LGPD)
│ └── responses.py # html_page, html_page_bare, security headers, CSP nonce
│
├── services/ # domain logic (pure-ish, take a conn)
│ ├── products.py # CRUD + soft-delete
│ ├── employees.py # CRUD + normalize_cpf + validate_cpf_format
│ ├── stock.py # add_stock, update_stock
│ ├── assignments.py # assign_epi, assign_multiple_epis (atomic)
│ ├── kits.py # role-based PPE bundles
│ ├── compliance.py # NR-6 90-day cycle
│ ├── audit.py # deletion_audit reader
│ └── dashboard.py # aggregates, shell_status
│
├── views/render.py # server-rendered templates (~1.6k LOC)
├── pdf/receipts.py # ReportLab PDF generation
├── static/app.css # design system tokens + components (~1.2k LOC)
│
├── tests/ # 36 tests, isolated_db + in-process server fixtures
├── screenshots/ # documentation assets
├── CRITICA.md # self-imposed technical audit (severity-ranked)
└── README.md # this file
Requirements: Python 3.12+ on Linux, macOS, or Windows. ~50 MB disk.
git clone https://github.com/leobaray/epi_system.git
cd epi_system
# Runtime deps (only ReportLab)
pip install -r requirements.txt
# Dev deps (ruff, pytest, requests)
pip install -e ".[dev]"
# Bootstrap an admin and run
EPI_ADMIN_USER=admin EPI_ADMIN_PASS=changeme python run.py
# → http://localhost:8140/Environment:
| Variable | Default | Purpose |
|---|---|---|
EPI_AUTH_ENABLED |
1 |
0 to disable auth (LAN-only setups) |
EPI_SESSION_TTL |
28800 |
Session lifetime in seconds (8h) |
EPI_COOKIE_SECURE |
0 |
Force Secure flag on session cookie |
EPI_ADMIN_USER / EPI_ADMIN_PASS |
— | First-run admin bootstrap |
Run tests:
pytest # 36 tests
pytest --cov=services --cov=core --cov-report=term-missingIn production at a single industrial site since 2025-09:
- 23 active employees, 21 PPE products in catalog, 854 units in stock.
- Every individual withdrawal — including high-turnover consumables like ear plugs and gloves — generates a signed PDF.
- Replaces a paper-and-spreadsheet workflow that left the site with no per-delivery PPE trail and out of compliance with NR-6.
In the first month in production, an auditor asked for the full PPE delivery history of one specific employee. The system produced the report in seconds; before, that record did not exist anywhere.
After several months in production, four items stand out. The full list with severity is in CRITICA.md.
- Extract a thin routing layer. The flat
if/elifladder inapp.pyworked while the URL space was small, but it has crossed the point where adding a route requires reading 50 lines around it for context. A ~30-line decorator-based router (no Flask dependency) is on the roadmap. - Async PDF generation. PDF generation runs synchronously outside the request transaction. Under bursty multi-user delivery (a full-shift kit assignment), it can block a request for 200–500 ms. An outbox table + worker would smooth that out.
- Replace f-string templates with Jinja2. Manual
safe_escape()is XSS-prone by construction in any non-trivial template. CSP buys time;Jinja2.Environment(autoescape=True)is the right long-term fix. - Move from raw
sqlite3to SQLAlchemy + Alembic — not for the ORM (queries are simple), but for versioned migrations. The current ad-hoc block incore/db.pydoes not handle column renames.
Solo developer, 21, Curitiba, Brazil. Built epi.manager in personal time as the only software engineer at the company that uses it.
Other projects in the same period:
- cnn — visual identification of automotive torque-converter models from a phone photo. Jetpack Compose Android client + PyTorch backend over FastAPI.
- magnum-site — the company's corporate site (Portuguese SEO #1 for several torque-converter terms).
- A RAG system over ~3,500 internal industrial documents (~45 pages each) with a "tutor mode" that adapts vocabulary to the user's technical level. Internal, no public repo.
Stack across projects: Python, Dart, Kotlin, TypeScript.
Looking for remote engineering roles with international teams.
- GitHub: @leobaray
- Workana: Leonardo Baray
- Email: leonardobaray@outlook.com
Licensed under the MIT License — see LICENSE.





