OverPowered International

OverPowered International

Project Management ·Jan 2024 → ongoing ·paused

OPI Project Management

A construction project-management platform that began as a request for a spreadsheet, built from the database up and run in production.

[ COVER ]
Deployment: paused Work: ongoing
Timeframe
Jan 2024 → ongoing
Tags
Project Management · multi-tenant · postgresql · fullstack
Credits
Xern Mottley — Engineer

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.

[ PORTALS ]
Three portals, one database Three doors at the top — internal, client, subcontractor — each with a line down through a band labelled row-level security into one database at the bottom. Each door sees only its own slice. THREE FRONT DOORS · ONE DATABASE · THE BOUNDARY IS IN THE DATABASE Internalthe company's own team sees: everything in its company Client portalthe people paying for the work sees: its own projects, their progress and invoices Subcontractor portalthe people doing part of it sees: its own commitments and pay applications row-level security every row carries its company · the database refuses the rest before the application sees it one Postgres database 184 tables and views · one schema · every company on the platform

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.

[ SCREENSHOT ]
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 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.

[ SCREENSHOT ]
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.

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 them wrong does not produce a bug. It 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.

[ TIMELINE ]
Commits per month, January 2024 to June 2026 Thirty monthly bars from January 2024 to June 2026. 321 commits, with work in 29 of the 30 months — May 2024 is the one silent month. Two months are highlighted: January 2025, when row-level security went on across the schema in a day, and May 2026, when the commitments and subcontractor layer arrived in a single 644-file commit. COMMITS PER MONTH · JAN 2024 — JUN 2026 321 commits across 30 months, and work in 29 of them 2024 2025 2026 RLS goes on — 95 tables in one day commitments layer — 644 files, one commit SOURCE: GIT LOG, xernM1/opi-pm · CUT AT JUNE 2026 — THE LATER COMMIT IS THIS CASE STUDY'S OWN DEMO FIX

Thirty months, and work in twenty-nine of them — May 2024 is the one silent month. The two highlighted are the days the story turns on: the boundary going in across the schema, and the commitments layer landing whole.

The record of that is not how many commits there were but what they went back to. Things were built, used, and rebuilt when use showed why — the client portal three times, the invitation flow three times in eight weeks, a PDF export three ways in two days. 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.

[ REVISIONS ]
Built, used, rebuilt — and what each rebuild corrected A dated log of 15 times the repository records something being done again, from November 2024 to September 2026, each with what the second attempt corrected. 7 rows, marked, are ones where the reason was written down before the fix: a review, an audit, or a specification. From December 2025 that is most of them. One row, marked, is the tenant boundary failing when it was switched on. Every row is a commit, a migration, or a written audit in the repository. BUILT, USED, REBUILT · AND WHAT EACH REBUILD CORRECTED 2024 11 27 payments split into paid and unpaid — one record had been answering two questions 2025 01 06 the tenant boundary, across the schema in a day — it recursed within the hour; the hotfix carries the same date 03 18 client portal, first pass — who may enter, decided in the middleware 04 14 invitations, rebuilt three times in eight weeks — the session never arrived; then the invitee had no account yet 04 15 client portal, second pass — who may enter, decided by whether a profile exists 06 04 client portal, third pass — layout and context rebuilt around the client 12 12 account deletion — a cascade ran both ways; found by diagnosis scripts, then removed 12 13 one money service for three screens — they had computed it three ways; one summed currencies unconverted 12 17 subscription dates drifting from the billing provider — gap analysis written; reconciliation doubled the same day 2026 01 20 pricing audited — eleven places stated a price, in the app, the database, the billing provider; now one 02 02 document upload — an audit found two requests with no indicator between them; an overlay the same day 02 03 PDF export, three ways in two days — a bundled browser, a smaller one, then the user's own print dialog 05 03 the commercial layer opens with cleanup — duplicate line items removed, a milestones table dropped; specified first 05 18 sixty-eight migrations land in one commit — thirty of them rollbacks, written before the change shipped 09 09 demo projects given a location — the weather card had been a dead end; found while capturing this page EVERY ROW IS A RECORD IN THE REPOSITORY — A COMMIT, A MIGRATION, OR A WRITTEN AUDIT FROM DECEMBER 2025 THE REASON WAS WRITTEN DOWN BEFORE THE FIX. THAT IS THE CHANGE THE LOG SHOWS found in use, then rebuilt the reason written down first — a review, an audit, a specification broke when switched on

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.

What it looks like in the finished thing: the project's financial view does not show one number. It shows two named lenses — what has actually been paid, and what unpaid invoices still claim — each stating 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.

[ SCHEMA ]
Every table brought under row-level security on 2025-01-06 A tree. One database at the root. Seven branches, one per migration phase, showing how many tables each brought under row-level security and how many policies it wrote. 95 distinct tables and 155 policies in a single day. The four base tables that may never call a helper function are named. ONE DATABASE · ROW-LEVEL SECURITY SWITCHED ON · 2025-01-06 95 tables155 policies, seven numbered migrations, one afternoon · two tables are counted in two branches Base — enabled, then hotfixed 10 tables · 25 policies profile · client_contacts · user_roles · business_users Identity 5 tables · 15 policies user_roles Financial 11 tables · 13 policies Project operations 14 tables · 20 policies Materials & inventory 17 tables · 17 policies Supporting 14 tables · 20 policies Audit, blog & CRM 26 tables · 45 policies NAMED: ONLY THE FOUR BASE TABLES THE RULE IS ABOUT. THE REST ARE COUNTED, NOT LISTED.

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.

[ BOUNDARY ]
The base-table rule Two panels. Left: a policy on the profile table calls a helper function, which reads the profile table, which re-triggers the policy — a cycle, drawn as a loop. Right: the fix. Fifty-five other tables may call thirteen helper functions; the helpers read four base tables; the base tables check the signed-in user directly and may never call a helper, so the dependency runs one way. TENANT ISOLATION · THE DAY THE HELPERS ATE THEMSELVES, AND THE RULE THAT CAME OUT OF IT THE CYCLE RLS policy on profile“can this user see this row?” a helper function“which company does this user belong to?” SELECT … FROM profile…which re-triggers the policy infinite recursion Eight tables had policies written and enforcement off. Turning it on surfaced this on the four tables the helpers themselves read. THE RULE the other 55 tables budgets, invoices, commitments, documents… 13 helper functions who is this user · which company · which projects may they reach 4 base tables profile · user_roles · client_contacts · business_users may call read may never call Base tables check auth.uid() directly, so nothing they do can re-enter a policy. The dependency runs one way and cannot cycle. DRAWN FROM THE MIGRATION THAT FOUND IT AND THE HOTFIX THAT FIXED IT, THE SAME DAY

The cycle on the left is what enforcement turned up within the hour. The rule on the right is what 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.

[ CODE ]
-- 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.

That is the lesson the whole project taught, and it was not get it right the first time. It was: 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

In production use since the start of 2024, and not finished — on purpose.

What began as one firm's internal tool became 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.

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 against live construction projects
20
Construction projects run end to end

Next — OPI Tech →

Need a platform your whole business runs on?