Your own rules, written down so that none of them can be broken quietly: the records, the states, the transitions, the roles — and the tests that fail when one of those rules changes.
The range
Scale: from one table to a system of domains
Four scales, smallest first: one entity with its validation and its roles; states and transitions; concurrent access with locks and a change log; domains with their own tables, permissions and tests.
One screen that replaces a spreadsheet
One table, a form with the validation actually behind it, a filtered list, an export, and two or three roles. Days rather than months. The screen is not the value — a spreadsheet has no constraints, no history and no idea who may change what, so two copies of it diverge quietly and nobody can say which one is right.
A process with states and transitions
A record that moves: quote to order, application to decision, case to close. The states are an enumeration, each transition is guarded by a policy that knows who is asking, and what happens on the way — a document rendered, a notification sent, a stock movement written — hangs off an event rather than off whichever controller happened to fire it.
A back office several people work in at once
Permissions scoped to rows rather than to menu entries, a trail of who changed what and when, documents generated from the data rather than retyped, and the systems already in use reached through a queue so their bad afternoon is not yours. This is the rung where two people editing the same row in the same second stops being hypothetical and starts needing a lock.
Many modules, one system
Stock, money, people and work in one codebase, each domain owning its own tables, its own policies and its own tests, with a plugin layer so a new payment or delivery provider is a drop-in rather than a core edit. There is one of these behind this page, and the section after next is what it looks like from the inside.
Effort follows how the rules intersect, not how many of them there are. One rule over a million rows is days. Four rules that contradict each other at month end is weeks, however many screens sit above them.
Approvals
A gate state: the record does not move on without sign-off, and a policy decides whose sign-off is required.
Roles and permissions
Access control at row level, not at menu level. A hidden link is not authorisation.
Scheduling and bookings
Rooms, staff, slots. The no-overlap rule is an integrity constraint in the database, not a check in the form.
Pricing rules
Per customer, per volume, per contract — computed once and fixed on the document line: an old order keeps the price it was sold at.
Documents
Invoices, acts and offers are generated from the data. One unbroken number series — no gaps, no duplicates — guaranteed at database level.
The systems already in use
Accounting, the bank, the messenger — asynchronous integrations through a queue, with retries and idempotency. Their downtime does not stop your process.
Reports
Aggregation in chunks over indexed columns: month-end close holds no locks on the tables being written to.
Stock as a ledger
An append-only movement ledger: every operation is an immutable row, and stock on hand is an aggregate over it, not a field to overwrite.
Staff records
Hires, absences and timesheets are events; totals are derived aggregates recomputed from the log, not stored by hand.
A pipeline
Leads, deals and cases on a state machine; deadlines are scheduler triggers, not display fields.
An API
Versioning, per-client authentication, rate limiting. The specification is generated from the same definitions the routes are built from.
Inherited code
Taking over a Laravel codebase somebody else wrote: characterization tests pin the current behaviour before anything changes.
The reference build
A system built to find the limit
An ERP platform: stock, finance, staff, sales and customers in one Laravel codebase. It is my own rather than a client's, and it is not running anybody's business yet — which is exactly why it can be taken apart here to a level of detail a client contract would keep off a public page.
Twelve domains, one codebase
Content, customers, sales documents, an omnichannel inbox, tenders, stock, staff and payroll, hiring, finance, taxes, internal tasks, and the platform core. Each domain owns its tables and its permissions — that boundary is what keeps a change in one from echoing in another.
An admin you can work in
One panel divided into workspaces, not a flat list of tables. A sidebar with an entry per table is a database browser, not an operator's interface — the difference shows the day somebody other than the developer sits down at it.
Every durable table sized for a volume it does not have yet
Indexes, pagination, chunked processing and an archival path are set when the table is created, not once it has slowed down. Files are sharded so no single directory accumulates thousands of siblings; the movement ledger rolls into monthly aggregates and prunes what they replace.
The test suite is the other half of the documentation
Nearly every recorded pitfall names the test that keeps it from coming back. The suite runs on two database engines, including the one production uses, and both runs must pass before a change is accepted.
Documented as it was built
An architecture map, a card per module, a route and data map, and a pitfalls file: what broke, why, and which test now guards it. Including the places where the codebase breaks its own rules, and why they are not fixed yet.
The archive holds 2 published projects built on Laravel, and on every one of them the printed development credit belongs to somebody else — I was art director there, and that is on the project pages. The system above is the one where the code is mine; it is what this page stands on.
Four mechanisms from this codebase; each was written after a real failure. A developer will recognise them at first sight. For everyone else: they are why software that is flawless in a demonstration still loses a record, double-charges a customer or runs the same import twice.
The race for the last one
A resubmitted form must not create two records, and a limit of a hundred uses must not let the hundred-and-first through. The row is held under a lock for the duration, and the claim is one conditional update that refuses once the limit is spent. Read, add, write back — that scheme loses this race and lets both through.
Money and stock get exactly one way in
Every movement of money or stock is written through a single path, and writing it twice is impossible rather than unlikely: each entry carries a fingerprint of its content, a unique index rules out the duplicate, and the database's own refusal is caught as the backstop for the race that got there first. Quantities and amounts are decimal, not float; a correction is a mirrored reversing entry, not an edit after the fact.
A slow job must not be started twice
Background work — imports, translations, document generation — has to survive its own slowness. Queues are split by duration, each with its own redelivery window, and a worker's timeout is kept strictly below the window of its queue — otherwise a slow job is handed to a second worker while the first still holds it, and runs twice. That relation is not a comment in the config; it is a test that reads the configuration and fails before such an edit can reach production.
A missed minute is replanned, not lost
Scheduled work — a nightly reconciliation, a reminder, a bank statement fetch — must not vanish because the machine was busy at the wrong moment. So the schedule is rows in a database, not a list of commands in code: every operation has its own policy for a missed run and for an overlap, runs are laid out as records ahead of time, and a tick picks them up once a minute. A separate heartbeat proves the scheduler is being called at all — one that has quietly stopped looks exactly like one with nothing to do.
A smaller example. A Telegram bot's address contains its own token, so a failed connection quotes the live secret in the error text and a standard report writes it straight into the log. The fix is a redactor that engages only when the failure actually carries a token; everything else keeps its original stack trace. Most of this work is that size.
The stack
What it is written with, and what runs on every push
Named rather than implied, versions included. Every line is read out of the reference build's configuration — composer.json, the queue settings, the CI workflow — and any of it can be checked by asking for the source file.
PHP 8.5 and Laravel 13
The current release of both, not the one that was current when the project started. Staying on the release cycle is a maintenance decision, not a taste: a framework two majors behind is a rewrite disguised as an upgrade, and whoever owns the code at that moment pays for it. Upgrades land as they are released, on a separate branch, with the suite as the acceptance test.
The admin is Filament, and that is a decision rather than a shortcut
Screens are declared in PHP: they are reviewed by diff and tested like the rest of the code, not kept in a builder nobody can read back. That buys time for business rules instead of table widgets. What it does not buy is a sidebar with an entry per table: the panel is split into nineteen workspaces, twelve of them settings, rather than one screen with a hundred fields.
Nine named queues on Redis, six worker groups under Horizon
Not one queue with everything in it. Work is separated by duration: the short queues share a ninety-second redelivery window, and long operations get their own, just over three hours. Why the window is tied to the worker's timeout is the section above; what matters here is the topology: notifications, media, translations and heavy operations have different latency needs, and one shared queue would let an import block a password-reset email.
Live push is Reverb where it is wanted, and polling where it is not
The transport is a setting and it defaults to polling: for a panel where being a couple of seconds stale costs nothing, that is the right default. Operator chat is the case that needs real push, and there it runs on the framework's own websocket server rather than a paid broker: one account fewer, one bill fewer, one outage fewer that nobody on this side can fix.
Permissions, media, settings, webhooks and schedule monitoring are packages
Spatie's, in all five cases, and they are named rather than hidden: a dependency somebody else maintains and thousands of teams run is a smaller risk than the same code written once and forgotten. What gets written by hand is the part your business actually differs in. A team that wrote its own permission layer has saved nothing — it has taken on an attack surface as homework.
Every change is checked three ways, on the build machine
Code style, static analysis, the test suite — and the suite runs twice, on SQLite and on MySQL: a query that passes on one engine and fails on the other is a bug that would otherwise surface on a production server. All three run on the runner, not on my laptop, which is why the result is something you can ask to see.
Everything past this point — where it runs, how it is deployed, who watches it and what is backed up — is the other half of the job, and it has its own page. This section stops at the build machine on purpose.
How it runs
Rules first, code second
A conversation about the process, not the software
What happens now, who does it, where it breaks, and what each failure costs. Most of the useful decisions are made here, and none of them needs a technology named.
Written down and agreed
The records, their states, who may move them, and what has to hold before a transition. The estimate is made against this document, and it stays yours whether or not the build goes ahead.
A first slice: one operation end to end
One record, one workflow, one screen — in the hands of the people who will use it. Whatever is wrong with the rules surfaces here, while changing them is still cheap.
Then modules, in the order that pays
Modules land one at a time, and each is usable the moment it lands. Nothing waits for everything.
Handover as a list, not a negotiation
The repository, the accounts and the written rules are yours from day one. The last step is checking that you can run it without me, not arranging that you cannot.
Questions
Before you write in
Possibly, and it is worth settling before anything else. Pages your team edits → WordPress. A process your business runs → Laravel. If it is the first of those, the WordPress page is where that work is described.
What the rules and their intersections weigh, not the screens. A handful of screens over one rule is a small job; four rules arguing with each other at month end is not. The estimate comes after the rules are written down — that is when it can be honest.
The first working slice is usually weeks. A system of several modules is months, and it arrives a module at a time rather than all at the end. Anyone giving you one number before the rules are written has not read them.
That is its own job — servers, backups, monitoring and releases — and it is described on the DevOps page. The application is built so that job stays ordinary.
Yes, and it is a common way this starts. Reading the code and saying honestly what state it is in comes first. Sometimes the answer is that it is fine and needs three fixes rather than a rewrite.
Yes, and for anything large that is the usual arrangement. Larger builds go to specialists I have worked with for years, and who did which part is printed on each project page.
No. It is the toolkit the thing is built with, the way a building has a structural system nobody inside it thinks about. What matters is whether your process has rules that are actually yours.
One honest caveat, since it decides whether any of the above applies to you. Custom software is the most expensive way to solve a problem that something ready-made already solves, and two of the commonest cases have a better answer elsewhere on this site.
This is a first pass over the rules of your process — clicks, not composition. Only the contact details are required; skip anything and I will ask it by voice. Answer everything, and the call starts from the middle of the conversation.