CSMS Wilmar: Three Years of Contractor Safety Compliance

CSMS Wilmar: Three Years of Contractor Safety Compliance

Table of Contents

Before a contractor is allowed to work on an industrial site, somebody has to establish that they can do it safely. Do their welders hold current certificates? Is the crane licensed? What happened the last time they were on site?

CSMS — Contractor Safety Management System — is where those questions get answered and recorded. I’ve been building and maintaining it since August 2023.

That’s the part I’d point at first. Not a launch, but three years and 300-odd commits of a system that people use to do their jobs.

What it covers

Thirty-eight modules, which sounds like padding until you look at what HSE compliance actually involves:

  • Pre-qualification — questionnaires, scoring, categorisation, certificates
  • Workforce — competencies per person, certificates with expiry, supervisors
  • Equipment — a register per contractor with category, specification, where it’s deployed, a photograph of the actual item, a working status of in-use or standby, and separately its licence and expiry
  • Incidents — accidents, causes, affected body part, severity
  • Observations and violations — HSE findings raised against a contractor
  • Safe man-hours, safety talks, housekeeping inspections, work permits
  • Social security reporting (BPJS TK), because compliance is not only internal

The domain language is Indonesian throughout — prakualifikasi, pekerjaan, peralatan, tenaga-kerja, pelanggaran-lk3 — and the admin interface ships in four languages: Indonesian, English, and both simplified and traditional Chinese, which follows from who the stakeholders are.

The dashboard is the point of the whole thing

Every module above exists to feed four numbers that an HSE department is measured on:

  • LTIFR — lost time injury frequency rate
  • AFR — accident frequency rate
  • ASR, reported both as lost days and lost hours
  • Total safe man-hours, the denominator underneath the other three

Each is scoped to a year, selectable independently, because safety performance is reported annually and last year’s figure has to stay quotable after this year’s data starts arriving.

Alongside them: personnel competency counts broken out by certification — Ahli K3 Umum, Ahli K3 Listrik, Ahli K3 Kebakaran, Scaffolder, Ahli K3 Konstruksi, Operator Excavator — each split into active and expired, plus job status, equipment licence status, and the same competency totals rolled up per contractor.

That expired count is the one people actually look at. A certificate that lapsed last month is the difference between a compliant contractor and a stop-work order.

The pre-qualification engine

This is the substantial piece, and the one worth explaining.

A contractor completes a versioned questionnaire. Questions are grouped into sections, each answer carries a score against a maximum, and answers can be marked not applicable — which removes them from both the numerator and the denominator rather than scoring them zero. That distinction matters: a contractor shouldn’t be penalised for not owning equipment they were never going to use.

The final score is a percentage of applicable maximum. It lands the contractor in High, Medium, or a failing category, with 60 as the pass mark.

A pass produces a numbered certificate in a CSMS/NNN/YYYY sequence, generated with a uniqueness check because two assessments completing in the same moment must not collide.

Assessments move through draft, submitted, in review, and completed, with separate assessor and verificator roles — the person who scores a submission isn’t the person who signs it off.

Two things I’d change, and haven’t

Honest maintenance notes, since a three-year project accumulates them.

The pass threshold of 60 is hard-coded in more than one place. Grid action, pass-status check, certificate generator. It should be one constant, or configuration. It isn’t, and every time the number comes up in conversation I check all three.

A method called generateCertificatePdf returns a PNG. It renders an SVG template through a raster converter; there was never a PDF. Renaming it means touching call sites in a system people rely on daily, so it has stayed, and the project’s own documentation now says so explicitly rather than letting the next person discover it.

I mention both because they’re the honest texture of long-lived software. The alternative version of this page pretends the codebase is spotless.

Certificates without a PDF engine

Certificates are a real design artefact — a layout somebody produced in vector form — so rather than rebuild it in HTML and hope, the system uses the SVG directly. Placeholders get replaced, then it’s rendered to a raster image.

One detail worth passing on. The renderer resolves fonts at render time, so on a server with no system fonts installed you don’t get an error, you get a certificate with invisible text. The fix is to ship the font files with the application and hand them to the converter explicitly, so output is identical regardless of host.

Assessment summaries are separate, and those genuinely are PDFs.

Multi-tenancy through query scopes

Contractor companies log in and see their own data. Administrators see everything.

That’s enforced with a global query scope: when the signed-in user belongs to a contractor, every query on the scoped models is constrained to that company. Two more scopes narrow data to an active month and year, both driven by a selector in the navbar and held in the session.

That year selector sitting in the header of every screenshot is the visible half of this. Change it and the dashboard, the grids, and the reports all move together, without a single controller passing a date around.

It’s a small amount of code doing a large amount of work, and it has one genuine hazard: the scopes apply silently. A query that returns rows in tinker returns nothing through the web, and the reason isn’t visible at the call site. That surprised me once, so it’s now the first thing the project’s guidance file explains.

The admin framework is the application

There’s an architectural decision here that I still think was right.

The whole system is built on dcat-admin, and not as an admin area bolted onto an app. The root path redirects into it, the conventional web routes file is essentially empty, and every controller lives under the admin namespace. Grids, forms, and detail views are declared through builders rather than hand-written.

For a system that is 38 role-gated CRUD modules with workflow attached, the admin framework isn’t a shortcut — it’s the correct shape. Writing forty resources by hand would have produced forty subtly different pages.

Learning that framework well enough to build this is also, directly, why I later wrote Steward — the same idea rebuilt in Go with generics, because I wanted the productivity without the loose strings.

Operational reality

Deployment is a push-triggered SSH deploy with staging and production mapped to two branches, running dependency install, migrations, and cache warming on the remote. Code quality is gated by Pint in CI. Tests are PHPUnit.

Two constraints I’d flag if you inherited it. There’s no asset build step in the deploy — compiled frontend assets are committed and shipped as-is, so a frontend change means building locally before pushing. And the test suite runs against whatever database is configured rather than an in-memory one, so the tests that refresh the database will happily wipe a real schema if pointed at one.

Neither is how I’d start a project today. Both are documented, which is the next best thing.

What three years teaches you

Most of the value in this system isn’t in any feature. It’s in the fact that the scoring rules, certificate numbers, and expiry dates from 2023 still mean the same thing today.

That only holds if you resist rewriting things. Every year there’s a temptation to modernise the stack, and every year the more useful work has been a new module, a fixed edge case, or a report someone actually needed.

Get in touch if you’re running compliance workflows on spreadsheets and it’s stopped scaling.

call to action

Ready to build your next project with me?

I’m ready to help you build, improve, and launch your next project — just drop a message and let’s get started.

Get Started Now