Compliance audit and inspection platform
Early hire in a founder's first week of engineering, and the only engineer after the founder stepped back from code four months in, owning the domain model, the admin and the reporting system.
A case study by Arif Hossen
Source: https://arifhossen.dev/work/compliance-audit-and-inspection-platform
- Role: Full-Stack Developer, remote contract, sole engineer from month four
- Team: A founder, then me alone
- Duration: About 17 months
- Stack: Laravel 9, Livewire 2, MySQL
- Period: Dec 2021 to May 2023
- Engagement: Remote contract
- 93% Of the repository's commits, 1,248 of 1,345
- 463 Files in the founder's first commit, before I joined
- 4 Automation files where every surviving line is mine
- 0 Automated tests. See the last section
Five days after the first commit
Context
The product is an internal web app for audit and inspection work. Staff create units of work, assign them, attach photos and documents, track status and priority, and report on what got done. Recurring work generates itself from a schedule. It is a responsive admin panel used by everyone, with no separate mobile app.
I joined five days after the repository was created. For about four months I worked beside the founder, who owned the repository, and a specialist who had written the scheduling package. Then the founder stopped writing code. I carried the codebase for the rest of the engagement and made 1,248 of its 1,345 commits. The accurate description is that I inherited the engineering and became its sole owner. I did not start it.
What was already there
Ownership
The founder's first commit was a generated admin scaffold of 463 files. It held 22 models, 37 migrations, 67 Livewire components, role and permission tables, and a whitelisted filter and sort trait that every index table uses. By the time I left there were 28 models, 49 migrations and 116 components. I did not write the filter trait, and I say so plainly because it is exactly the kind of thing that gets claimed by accident.
| Piece |
Whose |
| Admin scaffold, models, filter and sort trait |
The founder, in the first commit |
| Recurrence maths in the scheduling package |
The specialist |
| Report and reminder automation, four files |
Me, every surviving line |
| Livewire layer, views and exports |
Me, 95 to 97 percent of the commits that touched them |
From git blame and per-directory commit share on the final branch.
The rest of this page is what I built on top: the data for dashboards, saved reports and per-inspection records, the automation, the access control, and the screens that tie them together.
Templates must not rewrite history
Evidence
A checklist is used two ways. As a template inside a recurring definition, management wants to keep improving it. As a record inside one completed inspection, nobody should ever be able to change it. The scaffold kept both in one table, so rewording a template question or moving a limit silently altered every past inspection created from it. For a product that exists to produce audit evidence, that is a fundamental flaw. An auditor cannot trust a record that changes after the fact.
On one day in February 2022 I added a second, per-activity task table as the instance store and kept the original as the template store. Each instance links back to the template task it was copied from, so lineage survives without the template being able to reach forward. Every creation path now writes instances: manual add, add from a task group, and the scheduler. The detail and task reports read from the frozen records. The table is still called Task2.
<!-- diagram:frozen_records -->
Diagram: Templates and frozen records
A template is copied into each inspection on the day it is created, by hand, from a task group or by the scheduler. Reports read the copies. Editing the template changes nothing behind it.
Templates stay freely editable and completed inspections do not move. Every later feature, from task reports to out-of-range warnings, was built on that split.
Reports that email themselves
Automation
Supervisors looked at the same slice of data every day or week. Each of them re-applied the same filters, exported a spreadsheet and emailed it to colleagues by hand. I let a user save any filter combination as a named report, private to its owner, then mark it recurring on the same daily, weekly, monthly or yearly pattern the activity scheduler uses.
A command runs every minute, finds the saved reports whose occurrence is due, re-applies the filter, writes the spreadsheet and emails it to the owner. It compares each occurrence with the report's last run date, so one due run is processed once. Exports honour the active filter. They did not at first, and the fix came after a bug report. A second command sends a daily reminder of how many activities are pending.
For recurring work I built the template and trigger screens, the play and pause switch, and the step that copies a template's checklist and files into each generated activity. The recurrence maths and the generation command are not mine. I also guard against schedules set in the past, on the screen and again on the server.
Closing a hole nobody had filed
Access
The scaffold stored roles and permissions in the database, but the screens did not enforce them. A view-only user still saw edit and delete buttons. Through February and March 2022 I put gate checks on every action and button across roughly 36 modules and 144 permissions.
The founder's own checklist said that anything with a relationship must not be deletable: statuses, priorities, units, types, schedules and users. I added relationship checks to every list screen with plain-language messages, so no database error reaches a user. The primary statuses the workflow depends on cannot be edited or deleted. A deactivated user is logged out on their next request.
In June 2022 I found that uploaded evidence could be fetched without logging in. Nobody had filed it. I closed it with a dedicated media controller that serves files only to signed-in, verified users. A login gate is the first layer. The layer behind it checks each file against the record that user may see, and that is where I would start next.
Charts the customer arranges
Dashboards
The first screen a manager sees should answer the morning questions: how much work is open, by status, priority, type, project and person, and how planned dates compare with completion dates. Different customers wanted different charts, and the founder did not want each variation to become a request to me.
So a tile is a record. It stores the chart type, the data source, the field to group by, a label, an order, a row limit, a width and a visibility flag. Four chart components read a tile and draw it. An administrator adds, hides, drags and removes tiles without a developer, and a tile with no data shows a message instead of a broken chart. Branded installs for later customers shipped their own default tiles through seed data.
Plain-language cards in, reviewed work out
Working agreement
The founder was not a software person and the process had to work for him. Requirements arrived as Trello cards in his own words, with screen recordings for bugs. Long cards became acceptance checklists that I ticked as items shipped. I submitted a card for review and the founder accepted it, which happened 122 times. 277 of the board's 312 cards reached Done, according to the Trello export.
Commits name the screen and the behaviour, in small steps, so a card could be traced to the change that closed it. I worked from Bangladesh against the client's working day, and committed at every hour to match it.
No tests, and synchronous everything
What I would do differently
There are no automated tests for this work. The project owner directed the team to put feature velocity ahead of coverage, and I went along with it. What stood in for tests was the review gate and a staging environment. That is a process, not a safety net, and it cost me the freedom to refactor with confidence.
[!CAUTION]
Automation runs as synchronous scheduled commands with no queue. A slow export or a failed mail send happens inside the scheduler tick with no retry. The permission gates are rebuilt from the database on every authenticated request, uncached. Both were fine at this size and both are the first things I would change.