FireCRM

← Back to Portfolio

Jul 2026 – Present

A multi-tenant SaaS practice platform for UK fire and building-safety consultancies: CRM, projects, buildings and assets, assessments, floorplan markup, signed reports and debtors in one temporal system of record, forming a Building Safety Act golden thread. Currently in build.

Tech Stack

Laravel 13 PHP 8.3 PostgreSQL Inertia Vue Multi-tenancy Pest PDF Rendering

About this project

In build. This entry describes the design and the engineering; screenshots and a walkthrough follow once the demo build is deployed and signed off.

What it is

One system of record for a UK fire and building-safety consultancy: enquiries, projects, buildings and assets, assessments, documents, marked-up floorplans, issued reports, and the debtors that follow. Multi-tenant, so it works for one practice and sells to the next.

Why it needs to exist

These practices run across a scatter of tools: a CRM, spreadsheets for projects and assets, a document store, a drawing package for floorplans, and an accounts package that knows nothing about any of it. The work that ties them together is done by people remembering things.

The Building Safety Act made that expensive. A building now needs a golden thread, a continuous auditable record of what was assessed, by whom, on what evidence, and what has changed since. A practice whose history lives in five systems and a filing convention cannot produce one on demand.

The constraint that shaped the data model

In a regulated audit trail, "what does this record say" is the wrong question. The right one is "what did it say in March, who changed it, and when". A schema that updates rows in place cannot answer that, and logging bolted on afterwards does not recover it.

So the core is temporal by construction, a pattern I first built at Eseye:

  • Every entity is a slim indexed primary table, a typed attribute registry, and a history table.
  • Every row carries its tenant, a durable identifier constant across every version, the user who made that version, a validity period, and a lifecycle status.
  • Live tables hold current rows only. Superseded rows move to history.
  • Nothing is hard deleted and no row is ever edited. A delete closes the current row, writes the terminal state as a new row, and closes that too. Reconstructing any point in time is a query.

The part that makes it a deliverable

An assessment is finished when a client holds a document they can act on and an inspector can trace, so the chain runs the whole way. A pin on a floorplan becomes an annotation using the standard fire-safety symbol palette, and plans carry revisions with the annotations brought forward. The report engine renders the register and the marked-up plan onto the practice's letterhead. It names its author with the post-nominals their competence record holds, goes to a named reviewer with a queue, and is issued with its bytes checked against their own fingerprint and frozen at that moment.

Working on someone else's data

The practice's real material is in this project, which set a rule I would now apply to any client work: no repository gets a remote until an audit of both the tracked content and the full history comes back clean.

One verification pass reported clean when it was not. A shell quoting slip made the search read its own pattern as a file path, so it found nothing and said so cheerfully. Every scan after that ran against a deliberately planted canary first, to prove the check could still fail before its clean result was believed.

Where it is

Roughly 190 commits since early July, with a suite of around 510 tests. The foundation, the temporal core, the permissions spine and the CRM entities are in, along with the pipeline from enquiry to project, the money side through to an aged-debtor register, and the floorplan and reporting chain above. What is left before the demo is the hosted deploy.

Two rules throughout: documentation ships in the same commit as the code it describes, and a new test is proved able to fail before it is trusted.

What I would do differently

  1. Generate the demo data synthetically from the outset. Data derived from the real thing carries information you did not intend to publish, and inference is harder to spot than a name.
  2. Get an independent review in earlier. One component passed its own review, then failed an independent pass on four real defects. The sharpest was a queued job whose uniqueness lock ignored the tenant, so a second tenant's work was silently discarded at dispatch.