Demerara Mutual - Insurance
Publicly reachable, privately usable: an intranet for a four-territory insurance group
We replaced scattered internal requests and records with one employee intranet built around scoped permissions, an append-only audit trail, and controlled operational workflows.
Published · By Timothy Indarsingh
Case Study
Ordinary features, unusual constraints
Nothing in an intranet is exotic on its own. Requisitions, stock, a helpdesk, room booking, a document library, an employee directory: every organisation has these. At ours they existed as email plus spreadsheets plus institutional memory, in a regulated financial group that has to be able to prove who approved what.
The request that started it was ordinary too: let employees submit requests, raise tickets, read notices, acknowledge policies, and book rooms from home, on their phones, without being in the building.
The default answer in a financial institution is a VPN, and here that answer fails for an organisational reason. We are a three-person IT department supporting two legal entities across four territories, with no IT presence outside headquarters. Rolling out VPN clients, distributing credentials, and providing first-line support for a few hundred staff across four countries is out of reach for a department that size.
So the requirement inverted: make it publicly reachable, and make that safe. The project states its own design principle. The intranet is publicly reachable for employees but privately usable, and every meaningful action is authenticated, authorized, audited, and permission-shaped.
The core decision: identity and authorization are owned by different systems
The identity provider authenticates the person, and that is all it does. Every role, group membership, permission bundle, and per-user override lives inside the application. The reason: an identity-provider misconfiguration must never be able to grant business authority. If permissions live in your directory, then whoever administers the directory administers your approval limits, and those are usually different people, with different training and different change control.
The layers also verify each other. The core engine independently validates the token on every request and ignores permission headers forwarded from the gateway, even though the gateway is our own code. The second layer assumes the first might be wrong.
The rest of the access model leans toward boring, absolute rules. A deny beats every grant, unconditionally, with no precedence arithmetic. Expired grants are stripped before evaluation begins, so an expired deny can never be outlived by a lingering grant. Access changes themselves run through maker-checker with a mandatory reason and an optional expiry, so the system that grants permissions is itself permission-controlled and four-eyed. And the audit trail is append-only in the strongest sense: no application code path exists that could update or delete it.
The operational loop: requisitions → procurement → inventory
The forms were the easy part. The hard part was making the physical world and the record agree, including when two departments are promised the same box, when the invoice hasn't arrived yet, and when the wrong thing was delivered.
One non-negotiable rule
Stock increases on goods receipt and decreases only on manager delivery confirmation, never on requisition, approval, or purchase order. Every alternative timing lets the shelf and the system drift apart.
Five quantity states, not one
On hand, reserved, delivered pending confirmation, deducted, and available. "How many do we have" has several correct answers depending on what you're asking, and a system that collapses them into one number is how the same box gets promised twice.
The log is the truth; the balance is a cache
Stock is held as first-in-first-out layers consumed oldest-first, and every change (receipt, deduction, transfer, write-off, correction) is an entry in an append-only movement log. Corrections are new events; nothing is edited. The two legal entities' stock and costs never pool, down to the layer level.
Concurrency proven, not assumed
Allocation and idempotency are tested under real concurrent database transactions, against the real database engine, in tests marked to run separately. A test that runs against an engine which cannot exhibit the bug produces a green tick that means nothing. Two allocations racing for the last unit in stock is routine, so the code has to survive it.
A catalog that improves through use
A requisition line can be catalog-linked or off-catalog as a visible, first-class state, and an off-catalog item can be promoted into a real catalog entry through a review queue. The catalog gets better as people use it, rather than filling with junk or being bypassed.
Cost uncertainty is modelled, not hidden
Stock you've received but haven't been invoiced for is physically real and usable; the cost is what's uncertain. Cost moves through pending, proposed, and confirmed states, confirmed is immutable, and catalog prices refresh from invoices only.
The workspace domains: the requirements that matter are the negative ones
A directory, a noticeboard, a document library, a helpdesk and a room booker look like the easy part, the bit you'd buy off the shelf. Each is a distinct information-disclosure problem, and most of the real work sits in what people must never see.
Sensitive directory fields are excluded from list endpoints in the query itself. A list response carrying a field the caller shouldn't see amounts to a bulk export, and when the UI simply doesn't render the column, the leak is invisible. Announcement audiences are evaluated at read time instead of being fanned out to a recipient table, so the audience stays current against the real org structure. Protected documents are filtered out of category counts as well as listings, because a category that shows twelve documents and lists eleven has told you a restricted document exists, and roughly where. Room-booking conflict messages carry the resource and time window only, never the other booking's owner or purpose, since a conflict response is an information channel like any other. Report exports omit sensitive columns entirely, sit behind a permission distinct from viewing, and every export records which columns left.
Most systems block access and stop there. Concealing existence is the harder half, and in an insurance company it's the half that matters.
The review that assumed we were wrong, and was right
Every increment was planned before code, gated on research questions answered against the actual codebase, and then passed through a review ladder: tests alongside the code, a mandatory security review as a merge gate, independent review rounds over the cumulative diff, and a live smoke test against the running application. Findings were numbered and tracked to resolution. The commit history shows what the gate cost and bought: more documentation commits than feature commits, and a three-digit count of fix commits, nearly all of them findings from our own review gates before release, few of them reported by users.
Two findings are worth the space, because more tests would have caught neither. The tests would have been written against the same misunderstanding as the code.
First, a permission resolver in one domain only ever resolved at the broadest scope tier, so every narrower grant the specification described was silently non-functional. The permission keys existed, administrators could assign them, the UI showed them assigned, and nothing enforced them. A broken permission looks exactly like a working one until the moment it matters. The fix introduced scoped-then-fallback resolution, and the fix then caused a per-row query problem of its own, which was caught and closed with a per-request cache.
Second, an approval check quantified over a collection that could be empty, and all members of an empty set satisfy any condition. Where the check was meant to confirm that the actor is the specifically named second approver, it returned true whenever no second approver was configured at all, so any holder of the second-approve permission could clear a pending reservation alone. One reasonable-looking, idiomatic line switched off an approval control, and what found it was a reviewer asking what the code does when the list is empty. Both findings were closed before release, and nothing reached production.
On the project's status: the first two stages (identity, the requisitions-to-inventory loop, directory, communications, knowledge base, helpdesk, booking, and reporting) are built and tested. The organisational cutover, the parallel-run reconciliation and owner sign-off, is a business-process step, and it is still ahead of us.
Thirteen applications, one operating model
The platform now spans thirteen domain applications, but we resisted turning it into thirteen unrelated mini-products. The same legal-entity boundary, permission resolver, audit event shape, file controls and approval vocabulary run through them. That consistency is what lets an access administrator reason about a document library and a purchase approval without learning two security models.
Each domain still models its own awkward physical reality. A helpdesk transition carries a state precondition, not just a version number, so when two agents act at once the loser hears that another agent already moved the ticket, not merely that the record changed. A room reservation claims a set of resources, such as a room and its projector, and every resource is conflict-checked again when approval happens. A knowledge-base revision is a new row that supersedes the old one in the same transaction, preserving which version an employee acknowledged. Major policy revisions require a new acknowledgement; minor ones can carry the earlier acknowledgement forward.
We designed the exception surfaces with the same care as the happy path. Off-catalog requisition lines are visible states that can be reviewed and promoted into the catalog. Goods received without an invoice remain usable stock with an explicitly pending cost. Recurring room bookings materialise each occurrence and report the dates that could not be reserved instead of hiding conflicts inside a recurrence rule. These details stop staff from keeping a parallel spreadsheet for all the cases the official workflow cannot express.
That is also why the cutover remains separate from code completion. The applications can be built, tested and security-reviewed, while the organisation still has to run old and new processes in parallel, reconcile the results, name the operational owners and sign off. Calling that unfinished is more accurate than treating deployment as a technical switch.
The lesson
In internal software the dangerous bugs are the silent ones, and authorization bugs are silent by nature. A broken permission doesn't throw or log. From every angle available to an administrator, it looks like a working one.
The defences are structural: layers that check each other's work, precedence rules boring enough to reason about, an audit trail that physically cannot be edited, and an adversarial review before release that assumes you got it wrong. On this project, that reviewer was right.
Go deeper
Deep dive: permission bugs are silent
The resolver that ignored a scope tier, the empty-set check that disabled an approval control, and why negative requirements never show up in a demo.
Read the deep diveDeep dive: five stock states
How requisitions, purchasing and inventory stay aligned through reservations, FIFO layers, delivery confirmation and append-only corrections.
Read the deep diveDeep dive: five quiet information leaks
Why directory fields, document counts, helpdesk records, booking conflicts and exports all need explicit negative requirements.
Read the deep diveDeep dive: how the platform was reviewed
The bounded plans, research gates, decision records, security reviews and honest cutover criteria behind thirteen domain applications.
Read the deep diveCapabilities Demonstrated
Need a System Like This?
Let's discuss the workflow, controls, and outcomes your business needs.