Under the hood

Updated

Tech stack & architecture

Warehouse Lab is one Laravel application that plays seven systems. The four chapters under this heading are for readers who want to know how it is built: this one names every layer of the stack, follows a click through it, and shows where it all runs.

The stack at a glance

The server side is Laravel 13 on PHP 8.3, with PostgreSQL as the database. The pages are React 19 components written in TypeScript. Inertia connects the two: a Laravel controller answers with the name of a React page and its data, so there is no separate REST API for the interface and no client-side router to keep in step with the server. The first visit to a page is rendered to HTML on the server by a small Node.js process that runs the same React code; the browser takes over from there.

The look comes from shadcn/ui components on Base UI primitives, styled with Tailwind CSS 4. The Floor is a three.js scene inside the same React application. Queue, cache, and sessions all live in the database. Besides PHP and PostgreSQL, the installation therefore needs only Node.js for that rendering process: no Redis, no WebSocket server, no search engine.

In the browserReact 19Inertia 3TypeScriptTailwind CSS 4shadcn/uithree.jsRechartsHTML on the first visit, then JSON propsOn the serverLaravel 13PHP 8.3SSR on Node.jsFortifyWayfinderSchedulerQueue workerDomPDFEloquent modelsStored and sentPostgreSQLDatabase queueSessions and cacheMail
The stack in three layers.
BackendLaravel 13.34, PHP 8.3 locally and 8.4 in production, Fortify 1.40 for sign-in, Wayfinder 0.1 for typed routes
DatabasePostgreSQL 17 through Eloquent; about 50 tables from 73 migrations; SQLite in memory for the tests
FrontendReact 19.3, Inertia 3.7 with server-side rendering on Node.js, TypeScript 5.9 in strict mode, React Compiler 1.0
Interfaceshadcn/ui in the base-nova style on Base UI 1.8, Tailwind CSS 4.3, lucide icons, Geist and Geist Mono, Recharts 3 for the dashboard charts, driver.js for the guided tours
3Dthree.js 0.186 with React Three Fiber 9, drei 10, and the postprocessing library; models from Blender as meshopt-compressed GLB files
DocumentsDomPDF 3.1 for shipping and trolley labels, with Code 128 barcodes from picqer/php-barcode-generator
BuildVite+ 0.3 (vp), which bundles the Vite dev server and build, Oxlint, and Oxfmt
QualityPest 4.7 with the browser plugin on Playwright 1.63, PHPStan at level 7 through Larastan 3, Laravel Pint

One application, seven systems

The Shop, the ERP, the WMS, the Handheld, the Floor, this Documentation, and Feedback are one codebase, one deployment, and one database. Each system is a URL prefix with its own layout and sidebar: /shop, /erp, /mobile, /floor, /docs, /feedback, and the WMS on everything else. One login covers them all.

Inside, the business systems are kept apart as if they were separate products. The Demo ERP has its own tables with the prefix erp_, the Demo Carrier and the Demo Supplier have theirs, and the WMS has the rest. The WMS never reads an ERP table and the ERP never reads a WMS table. What one system knows about the other, it has learned from a message over HTTP, exactly as it would between two companies.

That is why the same article exists twice, as ErpArticle and as Article, and why an order number is the only thing an ERP order and a WMS order share. The chapter Backend & interfaces follows such a message.

How a click travels

The first visit to any page returns a complete HTML document: Laravel builds the props, the SSR server renders the React page into markup, and the props travel along so the browser can take over. From then on, Inertia turns every link and form into a background request, receives only the page name and its props as JSON, and swaps the React page in place. The server side looks the same in both cases: a route, middleware, a controller, and a response from Inertia::render(). The chapter Frontend & 3D follows the first visit step by step.

Controllers stay thin. They validate the input, often through a form request class, hand the work to an action, and redirect or render. The action holds the business rule and runs inside one database transaction. After a change, the controller redirects back with a flash message, which the layout shows as a toast.

Every page also receives a small set of shared props from HandleInertiaRequests: the signed-in user, the work counts for the sidebar badges, the number of articles in the Shop cart, and the state of the sidebar.

ClickInertia visitRouteroutes/web.phpMiddlewaresession, rightsControllervalidates inputActionone transactionReact pageprops as JSON
One request, from the click to the page.

Where the code lives

The repository follows the standard Laravel layout. As of October 2026 it holds about 22,700 lines of PHP in app, 65,200 lines of TypeScript in resources/js (almost a third of them the Floor), and 12,800 lines of tests.

`app/Actions`96 action classes in 17 areas such as Picking, Packing, Receiving, Integration, Crew, and Privacy. All business rules are here.
`app/Http`Controllers grouped by system (Api, Erp, Shop, Mobile, Carrier, Settings), form requests, and five middleware classes.
`app/Models`42 Eloquent models: 23 for the WMS, 12 for the ERP, and the rest for the carrier, the supplier, the Shop cart, and Feedback.
`app/Enums`22 backed enums for every status, type, and reason, each with its English label.
`app/Console/Commands`Ten Artisan commands: the scheduled runs, demo:reset, and helpers such as users:admin.
`routes`224 routes. web.php for the pages, api.php for the 16 interface endpoints, console.php for the schedule.
`resources/js/pages`101 React pages, in folders that mirror the URLs.
`resources/js/components`Shared components, the 40 shadcn/ui components in ui, the 55 files of the 3D scene in floor, and the figures of this Documentation in docs.
`tests`Feature, Browser, and Unit, plus gate.sh, the quality gate.

Conventions that hold it together

A few rules apply everywhere, and most of them are enforced by a tool rather than by discipline.

  1. One time zone: the application, the scheduler, and the PostgreSQL session all run on Europe/Berlin. A time is stored and shown as Berlin wall-clock time and never converted on the way.
  2. One home for rules: a controller, a console command, and the virtual crew call the same action, so a pick booked on the Handheld and a pick booked by the crew cannot behave differently.
  3. Statuses are enums, never free strings, and their labels are tested.
  4. Types everywhere: PHP methods declare parameter and return types and PHPStan checks them at level 7; TypeScript runs in strict mode.
  5. No hand-written URLs in React: every link and form uses a route function that Wayfinder generates from the Laravel routes.
  6. Formatting is not discussed: Pint formats PHP, Oxfmt formats TypeScript.

Where it runs

Production is a single server managed by Laravel Forge. A deployment builds a new release beside the running one, including the bundle for server-side rendering (npm run build:ssr), runs the migrations, and then switches over, so visitors do not see a gap.

Besides the web application, three background processes keep the Warehouse alive. The scheduler starts php artisan schedule:run every minute; without it, no order is released and the crew stands still. One queue worker (queue:work database --tries=3 --backoff=60) sends the emails for real Shop buyers. Interface messages are not queued: they are sent in the same request and logged.

The third process is the SSR server, started with php artisan inertia:start-ssr: a Node.js process that renders first visits. It listens on port 13714, which the firewall keeps closed to the outside. After each switch-over, the deployment stops it with inertia:stop-ssr, and Forge starts it again from the new release, so it renders with the new code. If it is down, the pages still work; the browser then renders them alone.

Development happens on Laravel Herd with PHP 8.3 and PostgreSQL 17. Locally, the Vite dev server renders first visits itself, so no separate SSR process runs. There is no build server. The quality gate described in Testing & quality runs on the developer machine before every push.

BrowserVisitorsOne server, Laravel ForgeWebPHP 8.4, LaravelSSR serverNode.js, first visitsPostgreSQLAll data, queue, sessionsSchedulerEvery minuteQueue workerSends mailMailShop buyers
What runs in production.