Skip to content
Case study · 11 / 15← All work

Grow ERP

An ERP for animal feed distributors: point of sale, e-invoicing, stock per branch, delivery routes and accounts.

Role
Full-stack development and maintenance, from the database schema to the server it runs on
Stack
Next.js 15 (App Router), React 19, TypeScript, Prisma + PostgreSQL, TanStack Table / Query, AFIP SDK, Docker Compose + Caddy, Vitest + Playwright
Point of sale
Point of saleServer Actions as the business layer, checked per action
(01) The problem

A pet and animal feed distributor ran its business across spreadsheets, notebooks and separate programs. It sells at the counter and wholesale, from more than one branch, and every invoice has to go through AFIP/ARCA. Orders get approved, picked in the warehouse and delivered by truck. The owner is not a programmer, so the system also had to be something the business could run and keep alive on its own.

My roleI built the product and kept it running in production for the business, through every change they asked for.
(02) ResultsCounted in the code
1,535commits, maintained continuously
105data models in the schema
93screens in the app
(03) Architecture

Three decisions that shaped it.

  1. 01

    Server Actions as the business layer, checked per action

    Why. The domain lives in 76 action files that pages call directly. Each one starts with protectAction and the list of roles allowed. Hiding a menu item is only visual; typing the URL by hand still hits the check. Nine roles, from owner to delivery driver, share one app.

    Trade-off. There is no general REST API. The online store and uploads need their own routes with separate auth, and a new action without the check would go unprotected.

  2. 02

    Customer debt is kept twice, on purpose

    Why. The account ledger is the source of truth for a customer's total balance. Each invoice also keeps its own pending amount, so payments can be applied to specific invoices and debt can be aged.

    Trade-off. Every money flow has to write both. If one is missed, screens disagree, so there is a reconciliation script that compares them.

  3. 03

    One server the owner controls, plus a guide to run it

    Why. The app, Postgres and Caddy run together with Docker Compose on a single VPS. A nine-chapter guide covers setup, backups, AFIP certificates and how to make changes with an AI coding assistant. A pre-push hook runs type checks, lint, tests and the build.

    Trade-off. One server means no redundancy, and backups depend on a cron job someone has to check.

(04) GalleryKeep scrolling →
Point of sale
Point of sale · Counter sales by barcode or name, with the day's totals for the register on top.
Sales and invoicing
Sales and invoicing · Every sale with its payment and invoice status, ready to request the AFIP authorization code.
Stock per branch
Stock per branch · Stock, value and minimums for the selected branch, with adjustments and transfers.
Delivery routes
Delivery routes · Today's route sheets per driver and truck. The map needs a Google Maps key, missing in this demo.
(05) What I'd do next
01Add foreign keys to the database. The schema uses Prisma's relation mode, so deleting a record can leave dangling references that break the POS.
02Run the Playwright suite in CI against a seeded database. Today the end-to-end tests are read-only smoke tests that need a real account.
03Move the pre-push checks to a CI pipeline, so a skipped hook can't ship a broken build.
Next project · 12 / 15
Tiendas →One store core and a Go CLI that spins up new client shops
Case study · 11 / 15

Grow ERP

An ERP for animal feed distributors: point of sale, e-invoicing, stock per branch, delivery routes and accounts.

Role
Full-stack development and maintenance, from the database schema to the server it runs on
Stack
Next.js 15 (App Router), React 19, TypeScript, Prisma + PostgreSQL, TanStack Table / Query, AFIP SDK, Docker Compose + Caddy, Vitest + Playwright
Grow ERP
(01) The problem

A pet and animal feed distributor ran its business across spreadsheets, notebooks and separate programs. It sells at the counter and wholesale, from more than one branch, and every invoice has to go through AFIP/ARCA. Orders get approved, picked in the warehouse and delivered by truck. The owner is not a programmer, so the system also had to be something the business could run and keep alive on its own.

My roleI built the product and kept it running in production for the business, through every change they asked for.
(02) Results
1,535commits, maintained continuously
105data models in the schema
93screens in the app
(03) Architecture

Three decisions that shaped it.

01

Server Actions as the business layer, checked per action

Why. The domain lives in 76 action files that pages call directly. Each one starts with protectAction and the list of roles allowed. Hiding a menu item is only visual; typing the URL by hand still hits the check. Nine roles, from owner to delivery driver, share one app.

Trade-off. There is no general REST API. The online store and uploads need their own routes with separate auth, and a new action without the check would go unprotected.

02

Customer debt is kept twice, on purpose

Why. The account ledger is the source of truth for a customer's total balance. Each invoice also keeps its own pending amount, so payments can be applied to specific invoices and debt can be aged.

Trade-off. Every money flow has to write both. If one is missed, screens disagree, so there is a reconciliation script that compares them.

03

One server the owner controls, plus a guide to run it

Why. The app, Postgres and Caddy run together with Docker Compose on a single VPS. A nine-chapter guide covers setup, backups, AFIP certificates and how to make changes with an AI coding assistant. A pre-push hook runs type checks, lint, tests and the build.

Trade-off. One server means no redundancy, and backups depend on a cron job someone has to check.

(04) Gallery
Point of sale
Point of sale · Counter sales by barcode or name, with the day's totals for the register on top.
Sales and invoicing
Sales and invoicing · Every sale with its payment and invoice status, ready to request the AFIP authorization code.
Stock per branch
Stock per branch · Stock, value and minimums for the selected branch, with adjustments and transfers.
Delivery routes
Delivery routes · Today's route sheets per driver and truck. The map needs a Google Maps key, missing in this demo.
(05) What I'd do next
01Add foreign keys to the database. The schema uses Prisma's relation mode, so deleting a record can leave dangling references that break the POS.
02Run the Playwright suite in CI against a seeded database. Today the end-to-end tests are read-only smoke tests that need a real account.
03Move the pre-push checks to a CI pipeline, so a skipped hook can't ship a broken build.
Next projectTiendas →
Nahuel SantillánEN