Skip to main content
Back to projects

Stockium

Shipped
Laravel 13PHP 8.4Inertia.jsVue 3TypeScriptPostgreSQL 18Tailwind CSSDockerPest
In use by the client · demo coming soon
Stockium

Overview

A multi-tenant SaaS for running electronics stores: every device is a serialized physical unit with an append-only history, from first intake through sale, repair or trade-in.

01

Problem

Electronics stores track devices in spreadsheets — the IMEI gets lost, history is rewritten in place, a cancelled sale simply disappears, and data from several stores blends together. Without an identity per physical unit there is no way to audit where a device came from, what its repair cost, or where it has been.

02

Solution

A Category → Product → Variation → Stock Item model where each item is serialized (IMEI, serial number) and moves through a closed status machine: available, reserved, in repair, awaiting pickup, sold. Every transition writes an immutable movement; cancelling a sale is a reversal that restores the previous status, never a delete; deleting a customer is anonymization that preserves foreign keys and the history required by Brazilian privacy law.

03

Architecture

A Laravel monolith with an explicit service layer and a Vue SPA served over Inertia — no REST or GraphQL API. Controllers stay thin (Inertia props and delegation), FormRequests validate with tenant-scoped exists rules, and thirteen services own the transaction, the locks, the movements and the audit log. Multi-tenant isolation lives in global scopes (TenantScope / UnitScope) applied by traits that also fill tenant_id and unit_id on create. The frontend consumes typed routes generated by Laravel Wayfinder; PDFs go to an S3 disk — MinIO in development, Cloudflare R2 in production.

04

Challenges

Making history genuinely immutable: stock movements and audit logs throw ImmutableRecordException on any update or delete. Ensuring tenant isolation never leaks through a query, and that concurrent operations — two sales of the same item, two signups on the same invite — can never produce inconsistent state, solved with transactions and pessimistic locks. Permissions are data, not code: new abilities go into the seeder and the can: middleware instead of checks scattered through the codebase.

05

Results

In production and in use by its first client, with automated deploys (Docker VPS + Caddy + GitHub Actions) and a single quality gate in CI — Pest, Vitest, PHPStan level 7, Pint, ESLint and Prettier behind one composer ci:check. Forty-five models, thirteen domain services and twenty seeded permissions. Public signup is closed: a tenant is only created through a one-shot invite (a 32-byte token stored only as SHA-256) or an Artisan command. A public demo is on the way.

Gallery

Click the image to view it full screen

1 / 5

Period dashboard and stock health