Depósitos CPS

The whole company’s stock in one place: what’s in stock, where it is and which site it went to.

RoleSole developer: data model, SQL core, backend, frontend and deploymentClientGrupo CPS, construction and aluminum joineryYear2026CodePrivate · internal Grupo CPS tool
Next.jsTypeScriptTailwind 4shadcn/uiSupabasePostgreSQLRLSCoolify
100% traceable

Stock is never corrected “by hand”: every change is born from a numbered voucher, with user and date. The number you see is the sum of its history. And negative stock is impossible by design, even under concurrent operations.

Internal system for Grupo CPS to manage materials and tools across warehouses and construction sites: purchase requests and orders, transfers, consumption vouchers with reconciliation, repairs and adjustments. Every operation is recorded in an immutable stock ledger with weighted-average costing, and the team sees the stock valuation, alerts and reports live.

Grupo CPS moves materials and tools across several warehouses and construction sites at once. That movement was tracked with spreadsheets, messages and memory: knowing how much of an item there was, and where, meant calling, counting and trusting.

The consequences were always the same: shortages discovered with the site already running, material “on the road” that showed up nowhere, rushed purchases with no record, and no reliable number for how much capital sat in stock or how much each site had consumed.

And there was a subtler problem: tools. An angle grinder is not “3 angle grinders”: it is this grinder, with its serial number, which is at that site, came back broken or got lost. And someone has to answer for it.

What I had to work around.

Several warehouses and sites operating at once

Concurrent movements from different places. The balance cannot depend on two people not posting at the same time.

Non-technical users, with different roles

Logistics operates; site managers and management query and request. Each one had to see and do only their part, without that depending on goodwill.

Own server, no third-party cloud

The company’s data stays on its VPS. That fixed self-hosted Supabase, Docker and Coolify, and left me in charge of operations too.

Numbers that must hold up

Stock valuation and spending per site are used to decide purchases and close sites. An estimate is useless: every number must be explained by vouchers.

Stock is never edited: it is recorded. Everything else derives from that.

The decision that organizes the whole system: nobody writes “40 bags left”. You record the movement that explains it (a received purchase, a transfer, a consumption voucher, an adjustment) as an entry in an immutable ledger: movements are insert-only, and correcting means posting the reversing entry, never deleting.

The balance per warehouse is maintained by the database itself, with a constraint that makes negative stock impossible even under concurrent operations. And what is on the road or at the repair shop is stock too: the virtual warehouses “In transit” and “Repair” guarantee that the sum of all balances always equals the company’s real stock.

On that foundation, costing uses a weighted average price frozen into each movement: transfers do not alter the average, and returns re-enter at the cost frozen in the original voucher. That is why the stock valuation on the dashboard is a defensible number, not an estimate.

Why it is built this way.

Append-only ledger

Movements are insert-only; correcting means posting a reversing entry. That single decision made auditing, voiding, period ledgers and valuation free: they are all queries over the same table.

The balance is kept by the database, not the app

A trigger updates the balance per warehouse on every entry and a constraint stops it from going below zero. Two simultaneous operations on the same item cannot leave negative stock, with no locks in the application.

Weighted-average cost frozen into each movement

The unit cost is fixed at posting time: transfers do not alter the average and returns re-enter at the original voucher’s cost. So the valuation never changes retroactively.

Transactional RPCs as the only write path

Every operation is a SECURITY DEFINER function that validates role and business rules inside a transaction. The movement tables have no write policies: even talking straight to the API, nobody can skip the rules.

Virtual “In transit” and “Repair” warehouses

What travels or sits at the repair shop is stock too, in its own warehouse. So the sum of all balances always equals the real stock and nothing ends up “nowhere”.

Tools as units, not quantities

Each tool has a serial number and its own life cycle: available → on site → under repair → broken or lost. Broken or lost ones are charged to the site that had them.

What I ruled out along the way.

Editing quantities directly

That is what the spreadsheets did. Without the history behind the number, there is no way to explain a shortage, void without deleting, or value the stock.

Business rules in the application layer

Any client talking to the API could skip them, and every rule would have to be repeated on every screen. In PostgreSQL there is one door and it has no shortcut.

Cloud-hosted Supabase

More convenient, but the company’s data would leave its server. Keeping control and not paying for a plan mattered more; in exchange, I run the operations myself.

A materials-only system

Without serial-numbered tools, half the problem (who has what, at which site, and who answers for it) would still live in messages.

The whole material circuit, from request to construction site.

  1. 01

    Purchase requests and orders

    Any user, site managers included, requests what they need with destination and deadline. Logistics builds the orders, which can mix suppliers per line and feed a price history per item and supplier.

  2. 02

    Partial receptions

    Purchases and transfers are received in batches. Closing with missing items is an explicit decision, with a reason: only then is what never arrived written off.

  3. 03

    “In transit” transfers

    Material moving between warehouses passes through a real intermediate state: while it travels it sits in neither warehouse, it is in transit, with a truck and people assigned to the trip.

  4. 04

    Consumption vouchers with reconciliation

    What goes out to a site gets reconciled: what was used is charged to the site’s spending, and what comes back re-enters stock at the voucher’s frozen cost.

  5. 05

    Tools by serial number

    Each physical unit has its own life cycle: available → on site → under repair → broken or lost. They are reconciled per unit, and broken or lost ones are charged to the site that had them.

  6. 06

    Per-user dashboard and alerts

    Each user builds their own home screen with the widgets they need, and notifications fire on their own: overdue or expiring requests, delayed transfers and stock below minimum.

The system also covers the boring-but-critical: a ledger view per warehouse or site for any period, one item queried across all warehouses at once, purchases in pesos or dollars with their currency recorded, and purchase orders as PDFs with signature lines, because the physical circuit with the supplier is part of the system too.

How it all fits together, in a diagram you can move around.

Five views with draggable boxes and a step-by-step walkthrough: who uses the system and what sits behind it, the material circuit from request to site, how every movement is recorded and valued, the life of a serial-numbered tool and how it is built.

Business logic lives in the database, not in the interface.

UI

Next.js + shadcn/ui

Operation and query screens: customizable dashboard, movements, ledger views and catalogs.

API

Self-hosted Supabase

PostgreSQL + Auth + API running on the company's own server, not on a third-party cloud.

RPC

Transactional RPCs

Every operation is a function that validates role and business rules inside the database, in a transaction.

LED

Immutable ledger

Movements are insert-only; correcting means posting a reversing entry. A trigger maintains the balances per warehouse.

SEC

RLS on every table

The movement tables have no write policies at all: the only door in is the RPCs.

OPS

VPS + Coolify

Automatic deploy on every push, migrations applied by CI, and daily backups of both server and database.

Hiding buttons in the interface is ergonomics; the real barrier is in PostgreSQL. Even talking straight to the API, nobody can skip the rules: every write goes through a function that validates everything on the database side.

A stock system is a trust system: every number has to be able to defend itself.

Roles with the bare minimum

The admin role operates and administers; the viewer role (site managers, management) reads everything and can only create its own purchase requests and customize its dashboard. And nobody can promote their own role.

Users closed by default

Public sign-up is disabled and profiles are born inactive, with no access to data, until an admin activates them from the app.

Who did what, always

Issuing, receiving, canceling, reconciling: every action is recorded with user and date, visible on every screen for every user.

Voiding is not deleting

Voiding posts the reversing entry and the history stays complete. Nothing gets corrected quietly.

In numbers.

100%

of balances traceable: every number is the sum of vouchers with user and date.

0

negative stock possible, enforced by a database constraint, even under concurrent operations.

In production

used by logistics, site managers and management at Grupo CPS, on the company’s own server.

The core is tested where it lives: in SQL.

SQL core tests
Integration suite that walks the full circuit, inside a transaction that rolls back
What it covers
Numbering, weighted-average costing, partial receptions, reconciliations, serial-numbered tools, RLS and roles
Negative stock
Impossible by design, even under concurrency
Versioned schema
Migrations applied by CI on a clean database before every deploy

The suite runs inside a transaction that rolls back at the end: it tests the real core against the real database without dirtying it. If the rule lives in PostgreSQL, so does the test.

The system from the inside.

Home dashboard of the Grupo CPS warehouse system
The home dashboard: stock valuation, movements of the last 30 days, stock per warehouse, spending per site and alerts for overdue requests, delayed transfers and stock below minimum.
Warehouse ledger view with period balances and movements
The ledger view per warehouse or site: period balances, every movement with its voucher and user, and voidings as reversing entries. Nothing gets deleted.
Purchase order generated as a PDF ready to sign
A purchase order as a PDF, ready to print and sign with the supplier: the physical circuit comes out of the system too.
  • If people do not believe the number, they go back to the spreadsheet. That is why everything is built so every balance can be explained by its vouchers.
  • Modeling movements instead of quantities simplified everything else: ledger views, auditing, voiding and valuation all fell out of the same design.
  • Putting business rules in the database, and testing them there, means security does not depend on which buttons the interface shows.
  • The physical circuit is software too: the purchase order PDF with signature lines matters as much as the table that generates it.

Want to talk about this project?

The code is internal to the company, but I can explain how it is designed. Write to me if this interests you or you want to talk about a role.