Project Management ·Jan 2024 → ongoing ·live
OPI Project Management
A construction project-management platform that began as one firm's request for a spreadsheet and was built from the database up. The firm ran twenty projects on it end to end. Live on new hosting since September, it is now being made ready for other firms, with each claim the product makes checked against what it does.
- Timeframe
- Jan 2024 → ongoing
- Tags
- Project Management · multi-tenant · postgresql · fullstack
- Credits
- Xern Mottley — Engineer
- Links
- Visit the live site ↗
- The kind of build
- Venture Build one like it →
Where it started
A construction firm was running projects the way most small construction firms do: spreadsheets, paper reports coming back from site, and whatever each supervisor kept in their own book. Nobody had one trustworthy view of what a project was costing against what it had been budgeted — while it was still running, and the number could still change something.
The ask was a spreadsheet to help track projects.
What was done
A platform instead — from the database up, on one schema.
Projects and tasks. Budgets and budget-versus-actual. Expenses, invoices and invoice OCR, payments and outstanding balances. Subcontractor commitments and pay applications. Materials, equipment, field reporting, health and safety.
It serves three audiences that share almost nothing, through three front doors onto one database. The field files a report from a tablet on site. The office reconciles it against a budget line on a full screen. Clients and subcontractors each get their own portal, and each sees only its own slice.
Three front doors, one database. The band between them is the boundary — every row carries its company, and the database refuses the rest before the application ever sees it.
The site's own surface, cropped to the cards: construction log, HSE events, equipment on site, personnel, materials. An office tool would carry none of these.
The same project at a desk — the Commercial Hub through its first lens. Two named lenses rather than one number, each stating what it answers and what it does not.
Deciding which of those a given screen is for is most of the design work. Every one of them gets a guided tour, built per context — including the subcontractor, who is furthest from the office and least likely to have chosen this software. There is a demo project the platform generates for itself, so nobody learns on live data.
A later layer is the business console, behind its own login inside the same application: CRM and leads, marketing, a content pipeline with its own monthly spend cap, a support desk, per-tenant observability. It is there because running the platform and running the business around it turned out to be one job.
The decision that mattered
The requirements never stopped moving, and neither did the person building them.
This was a first real project, started straight out of a degree, inside an industry that had to be learned at the same time as the software. What made that survivable was putting usage first and asking the same question of every screen: how does this work on a site, and how does this work at a desk. The two answers are rarely the same, and getting one wrong produces a feature nobody opens.
Some answers took whole detours. The client portal came in three passes across a year, each partly redoing the last. The commitments and subcontractor layer arrived in one commit that touched six hundred files.
What the commits went back to is the better record. Things were built, used, and rebuilt when use showed why: the invitation flow three times in eight weeks, a PDF export three ways in two days.
Fifteen times the record says something was done again, and what the second time fixed — drawn from the commits, the migrations and the written audits. The green rows are the ones where the reason was written down before the fix; from December 2025 that is most of them.
And from the end of 2025 the reason started being written down before the fix: a review of how three screens computed money, an audit of every place a price was stated, a diagnosis of why an account could not be deleted. By the time the commercial layer landed it arrived as a written specification, then a plan, then sixty-eight migrations in one commit, thirty of them rollbacks.
In the finished thing, the project's financial view shows two named lenses where one number would be usual: what has actually been paid, and what unpaid invoices still claim. Each states what question it answers, what it does not, and how moving money changes the other. Cash against accrual is the commonest way a construction budget gets misread. The screen argues with the misreading before anyone can make it.
The sharpest instance was the day the founding assumption expired.
The system was built for one company, in-house. There was nothing to isolate, so there was no reason yet to know much about row-level security. When it went multi-tenant that stopped being true, and the boundary had to go in underneath a system that was already running: across the whole schema, in seven numbered migrations, in a single day.
One database, seven branches, one afternoon. Each branch is a migration phase; the bar is how many tables it brought under row-level security. Ninety-five tables and 155 policies before the day was out — the four named are the ones the rule below is about.
It broke immediately, in a way that is only obvious afterwards. The policies read a user's company through a helper function. The helper reads the profile table. The profile table has a policy. The policy calls the helper. A query for one row recursed until Postgres gave up.
First the cycle that enforcement turned up within the hour, then the rule that broke it: base tables check the signed-in user directly and may never call a helper, so nothing they do can re-enter a policy.
The rule the schema still runs on came out of that afternoon: the four tables the helpers themselves read check the signed-in user directly, and may never call a helper. Everything else may. The dependency runs one way, so it cannot come back around.
-- Problem: Helper functions query base tables (profile, user_roles) which
-- triggers RLS evaluation, which calls helper functions again = INFINITE RECURSION
--
-- Solution: Base tables (profile, user_roles, client_contacts, business_users)
-- CANNOT use helper functions in their RLS policies. Must use direct checks.
CREATE POLICY "…" ON profile
FOR INSERT
WITH CHECK (
-- check the role directly, without a helper function
EXISTS (
SELECT 1 FROM user_roles ur
WHERE ur.user_id = auth.uid()
AND ur.role_id IN ( /* the privileged roles */ )
)
); The rule, written the day it was found — the comment is the evidence. Abridged: the privileged role ids are not the point and are not published.
In the autumn of 2026 the question became whether another firm could buy it and run on it. The honest answer was not yet, and the work since has been to prove each claim before making it.
The public site lost every line it could not back, from free forever to a phone app that did not exist yet, and a test now scans the site for any that come back. Inside, a dashboard tile was showing the profit margin as revenue growth, and a rising cost was drawn in green. Each figure stays off the screen until it compares like with like.
Then every surface. The live site had fallen a month behind the code, so a claim removed in October was still public; releases are now built from a tagged version of the code, three in four days. Every screen, email and export was then listed from the code, 140 in all, each scored the same way and every gap given an owner and a date.
Thirty-four months, with work in thirty-one of them; May 2024, July and August 2026 are silent. The two highlighted are the days the story turns on: the boundary going in across the schema, and the commitments layer landing whole. The tallest, September and October 2026, are the move to new hosting and the work of making it ready to sell.
A rebuild is under way, and its data platform was chosen the same way, by a trial built and measured in a day against five written bars. The last bar, speed, was closed on an estimate, because the accuracy of the data and its security come first. In the rebuild each company has a database of its own, so the boundary that once had to be put in underneath a running system is the shape of the storage itself.
What the whole project taught is to build so that the thing you have not learned yet can be put in underneath, without taking the system down while you do it.
Where it stands
Live at opi-pm.com on new hosting since 24 September, after a move that took a day and a half.
The first firm's twenty projects are its track record. What began as that firm's internal tool is now a product with a business attached to it, and the business console is where that ended up as software: the sales pipeline, the marketing, the support desk and the books all became things the platform had to do, so they became things the platform does.
The rebuild runs on staging, made for many firms from its first line, and the live app moves across when it is ready. The next milestone is the first firm beyond the one it was built for.
Iteration has not stopped and is not supposed to. The small things nobody notices when they are handled are the same ones everybody notices when they are not.
- 1,474
- Transactions recorded on the first firm's projects
- 20
- Projects the first firm ran on it end to end
Next — EBL →