Skip to content
← All projects
Paid client projectLive in production

LICS — ERP for Laboratorio de Inyección Castro Solís

Offline-first ERP that replaced a legacy DBF system for a real fuel-injector lab. Live in production today.

Role: Solo developer — design, backend, frontend, infrastructure and deployment

Team

Solo

Timeline

Jul 2026 – Present

Type

Client

LICS login screen

Login

LICS main dashboard

Dashboard

LICS inventory module with stock movement history

Inventory & movement history

LICS confirmed sale detail, with invoice download and cancellation

Sale detail

LICS purchase import cost calculation

Import cost calculation

LICS injector service tracking

Service tracking

LICS generated report

Reports

LICS printed barcode label

Barcode labels

01 · Context & Problem

What problem this solves

Laboratorio de Inyección Castro Solís, a real diesel and gasoline fuel-injector lab, ran on a legacy FoxPro/DBF system (tables named INVEN01 through INVEN08) with no dedicated IT staff and unreliable internet. They hired me directly, as freelance work, to replace it with something a non-technical operator could run fully offline, trust for audit purposes, and restore themselves if something broke.

02 · Solution

What it actually does

Django REST Framework + PostgreSQL 17 backend split into service and selector layers per domain, a Next.js frontend, packaged as a native Windows desktop app via Electron and WSL2 so the business can run with zero dependency on internet access.

  • Inventory with stock derived entirely from an append-only movement history (StockMovement) — no product ever stores its own stock count as an editable field.
  • Product variants and equivalences: multiple product rows can share a standard_code to represent original/generic/equivalent parts, each with independent price, stock and location.
  • Purchasing and import costing: draft/confirmed/cancelled purchase lifecycle, per-cost exchange rates, and an append-only cost/margin/suggested-price history per purchase.
  • Sales with a draft/confirmed/cancelled state machine; confirming or cancelling row-locks the sale and re-validates stock inside an atomic transaction before touching inventory.
  • Injector service tracking with a real state machine (received → in progress → ready → delivered), capturing technical fields (resistance, leakage, inductance, isolation) and consuming accessory stock automatically.
  • Weekly cash closing: one snapshot per week, computed and frozen from confirmed sales and delivered services in that date range.
  • Physical inventory counts with draft capture and an approval step that generates reconciling stock adjustments.
  • Eight built-in reports: low stock, stock by location, product movement history, supplier pricing, purchases by supplier, sales by date, top-selling products, top customers.
  • PDF documents and Code128 barcode labels generated locally with ReportLab — no external service required.
  • Universal search across products, locations, suppliers, customers, sales and services.
  • A dedicated legacy-migration module that moved 73 suppliers, 3,655 products and 2,079 purchases from the old DBF system into the new schema, with its own staging, validation and issue-tracking tables.
  • Role-based access control with five fixed roles (admin, inventory, sales, customers, read-only), enforced per module and per HTTP action, not just by staff/non-staff status.

03 · Architecture

Architecture & decisions

Docker Compose stack (Django REST Framework, PostgreSQL, Next.js, Nginx) running inside WSL2 on the client's own Windows machine, wrapped in an Electron shell so it looks and feels like a normal desktop application.

LICS deployment architecture: Electron desktop app, Nginx, Next.js and Django REST Framework, PostgreSQL

Stock is never a stored, editable field — only movements.

Guarantees traceability: cancelling a sale creates a linked reversal movement instead of editing or deleting the original record, so the audit trail is permanent.

Nginx is the only port exposed by the stack.

The backend, database and frontend containers are Docker-internal only, minimizing the attack surface on a machine that's also used for other office tasks.

WSL2 + Docker Engine + Electron instead of a Linux kiosk machine.

The client's PC is also used for Office and other day-to-day tasks, which ruled out a dedicated Linux kiosk box. This runs the same Docker stack on hardware the client already owns.

No SSH access in production.

All administrative actions run locally through the Electron app or a person physically at the machine, which was judged a net security improvement over the original SSH-based remote-support model.

Legacy part-equivalence grouping reused the existing product-variant model instead of a new table.

The variant model (shared standard_code, variant_kind, an add-variant endpoint) already existed unused since an earlier migration — the 2026 equivalence-grouping work only needed to populate it, not extend the schema.

04 · Stack

Full stack, by layer

Frontend

Next.js 16.2.10React 19.2.4TypeScriptTailwind CSS 4

Backend

Django 5.2.6Django REST Framework 3.16.1Python 3.13

Database

PostgreSQL 17

Infrastructure & Deploy

Docker Compose (dev & prod)Nginx 1.28Gunicorn 23Electron + WSL2 (Windows packaging)GitHub Actions (self-hosted Windows runner, installer build)

Auth

Django User/Group/PermissionDRF Token authentication5 fixed role groups

Testing

Django TestCase, 600+ automated tests

05 · Challenges

Technical challenges solved

WSL2 silently powering off the whole distro

Problem: In production, the WSL2 distro running the stack would intermittently shut down on its own, taking the whole application down with it.

Solution: Root-caused through systemd/journalctl log correlation to a missing keep-alive client — WSL2 shuts its distro down when no process stays attached. Fixed with a scheduled task that holds a persistent WSL client open, then hardened further after a recurrence.

Electron/Chromium losing window focus

Problem: The Electron window's web contents wouldn't reliably regain focus after being refocused, a known unresolved issue upstream in Electron/Chromium.

Solution: Mitigated (not fully fixed, and documented as such) with a focus handler plus a polling refocus check every 1.5 seconds.

Migrating real legacy DBF data with corrupted files and bad dates

Problem: The client's legacy DBF files included a genuinely corrupted sales table (confirmed identical across two independently supplied copies) and a Y2K-style two-digit-year bug in purchase dates.

Solution: Wrote a custom DBF reader to handle files that a standard library mis-terminated on stray bytes, excluded the corrupted table from migration after byte-level verification, and applied a deterministic year-correction rule validated against the resulting date distribution.

Preventing double-confirm and double-cancel races on sales

Problem: Concurrent requests to confirm or cancel the same sale could race and corrupt stock movements.

Solution: Every confirm/cancel operation row-locks the sale with select_for_update() inside an atomic transaction and re-validates the state machine before touching stock.

06 · Results

Results

Replaced a fully manual, DBF-based workflow with a system now running the lab's day-to-day inventory, purchasing, sales and service tracking in production, after migrating the client's real historical data.

No hard metrics confirmed yet for this project — the impact above is qualitative only.

07 · Retrospective

What I'd do differently

Packaging a Linux-first stack (Docker on WSL2) for a single non-technical Windows user surfaced real OS-level problems — silent WSL2 shutdowns, an Electron focus bug — that don't show up in a normal multi-machine deployment. Hardening for one physical machine with no IT staff behind it is a genuinely different problem than hardening for a server fleet.

Have a system that needs building?

Tell me what you're running today and what's breaking. I'll tell you honestly whether I can help.