Back to the evidence

Case study

A Conservation Leadership Training Platform

A multi-tenant capability platform for conservation organisations — protected-area authorities, ranger corps and training colleges that need to know what each role requires, where every person stands against it, and which training closes the distance.

Role
Contracted engineering. Architecture through handover.
Duration
Eight months, four delivery milestones.
Stack
Django, PostgreSQL, React, TypeScript, Docker.
Status
Built and tested; in final assurance.
Provenance

The original draft of this case study was written by hand. It was then structured and expanded with AI assistance, and reviewed and approved by us before publication. The judgements, the decisions and the mistakes described are ours.

The problem

A ranger corps cannot tell you what it knows.

Protected-area organisations run distributed workforces under real constraint: remote sites, intermittent connectivity, thin administrative capacity, and turnover that takes institutional knowledge with it. They can tell you headcount and payroll. They cannot tell you, with evidence, what a section ranger is required to be able to do, how far any individual stands from that, or whether the training budget is buying the capability they actually lack.

The information exists. It sits in spreadsheets on individual laptops, in training certificates in filing cabinets, and in the judgement of long-serving managers. None of it survives a resignation. When a capable person leaves, the organisation loses both the capability and the only record that it existed.

The brief was to replace that arrangement with a system where role requirements are defined once, progress is recorded against them continuously, and the resulting picture is available to the people who have to act on it.

Capability dashboard showing a 65 percent organisational readiness score, five departments tracked, department capability bars, and a ranked list of the most common capability gaps.
What the system produces. Organisational readiness rolled up from individual records, department scores banded red/amber/green, and the specific learning outcomes most employees have not yet met — here, critical thinking, missing for 14 of 28 people. Screenshots on this page show a demonstration tenant seeded with fictional organisations and people.

Architecture

One definition, four consumers.

The centre of the system is a competency framework two levels deep: six modules, each expressed at three levels of development, resolving into 117 discrete learning outcomes. Every other capability in the product is a view onto that one structure rather than a parallel implementation of it.

This is the decision the whole build turns on. Role requirements, individual progress, gap analysis, succession ranking and career roadmaps all resolve to the same outcome records — which is why the numbers on different screens agree with each other, and why adding a new consumer costs a query rather than a schema.

Module × Level 6 × 3 defines Outcome 117 records required by Job requirement per role enrolment generates Progress record per person × outcome read by Gap analysis Succession ranking Career roadmap
The framework chain. A module at a development level defines an outcome. A role selects the outcomes it requires. Enrolling a person generates one progress record per required outcome — a database signal, not an import step. Everything downstream reads those records rather than keeping its own copy.

Scoring

Measuring a gap is a design decision, not a calculation.

Coverage for a person against a module is completed outcomes over assigned outcomes, banded at fixed thresholds: 80% and above is green, 40% and above is amber, below 40% is red. Work in progress counts zero. A half-finished module is not half a capability — either the person can do the thing or they cannot.

The consequential decision is what happens when nothing has been assigned at all. The obvious implementation returns 100%: no requirements, nothing missing, everything satisfied. That is wrong in a way that hides exactly the problem the system exists to expose — a role nobody has defined requirements for looks perfectly staffed. It returns zero instead, and the comment in the source says why.

Completed outcomes ÷ assigned outcomes, at the person’s development level Red Amber Green below 40% 40% and above 80% and above 0 40 80 100 Work in progress counts zero. A half-finished module is not half a capability.
Fixed bands, not a curve. The thresholds are constants, so a department score means the same thing in every organisation on the platform and in every reporting period. Nothing is normalised against a cohort.
From the gap analysis service: 0.0 when no targets assigned — not 100% (no requirements ≠ fully covered)
Gap analysis matrix with employees as rows and the six framework modules as columns, each cell showing completed, in progress, or not started.
Employees against modules. Each cell is one person's standing in one module at their role's development level. Blank cells are the honest answer where a module carries no requirement for that role, rather than a score implying coverage that was never asked for.

Readiness

Succession from evidence rather than recollection.

Asked who could step into a section ranger post, most organisations answer from memory. The platform answers by counting: for every employee, how many of that role's required outcomes they have actually completed. The ranking below is computed from live progress records, and the fraction beside each score is the working — nineteen of twenty-two requirements met.

Succession planning screen ranking candidates for the section ranger role by readiness score, showing scores of 93, 89, 89 and 84 percent with the fraction of requirements met and a count of remaining gaps.
Candidates ranked for one role. The score is completed required outcomes over total required outcomes; the gap count is what remains. Because it reads the same records as the gap analysis, a completion recorded this morning changes this ranking this morning.

Two scores, deliberately different

The system computes readiness two ways and it is worth being precise about why. The career and succession path is binary: an outcome is met or it is not, because promoting someone on half-finished evidence is how the old spreadsheet failed. The analytics engine is weighted, giving half credit for work in progress, because a capability trend that ignores everything currently underway shows a department as flat while it is visibly improving. Same records, two questions, two defensible answers — and they are never presented as one number.

Tenancy

Where a forged request actually dies.

Every organisation on the platform holds isolated data, and the tenant is named in a client-controlled request header. That header is trivially forgeable, so the interesting question is not that isolation exists but which layer refuses the request — and that answer had to be exact rather than reassuring.

Resolution and enforcement are separate. Middleware resolves the header to an organisation before authentication runs, which means it cannot check membership itself. Membership is therefore enforced inside the authentication class — the single point every authenticated request passes through, regardless of what permissions an individual view happens to declare. Views then scope their own queries to the resolved organisation, and soft-deletion is filtered by default at the manager layer.

Request JWT cookie + org header 1 · Middleware resolves the org 404 if unknown 2 · Authentication is this user a member of that org? every request passes here yes 3 · View query scoped to that org Data no 403 — never reaches a view
A forged organisation header does not reach a query. Because resolution happens before authentication, the membership check sits in the authenticator rather than the middleware — one chokepoint that no view can forget to apply. Cross-tenant access is covered by a dedicated test suite that asserts an organisation's data is invisible to a member of another.

Assurance

The tests were lying, and that was the finding.

Midway through the build, the coverage instrumentation was found to be measuring the wrong thing on both sides of the stack. The frontend number was the serious one: it had been reporting a comfortable figure while true coverage was a small fraction of it. A static analysis tool was also passing cleanly for the best possible reason — it was scanning no files at all.

All three were configuration faults rather than code faults, which is exactly why nothing had surfaced them. A green check that is not measuring anything is worse than no check, because it actively purchases confidence. Each was fixed, the true position was measured and stated plainly, and coverage was then raised against honest numbers rather than flattering ones.

The suite now stands at 861 backend tests across 41 files and 158 frontend tests across 21. A security hardening pass followed, and its findings were closed behind regression tests so the same defects cannot return quietly.

ControlAs configured
Access / refresh token lifetime60 min / 14 days, rotated with the old token blacklisted
Token transportHttpOnly cookies, with double-submit CSRF on unsafe methods
Concurrent sessionsCapped at 5; exceeding it evicts the oldest rather than refusing the newest
Session window24 h rolling, reset on activity, expiring at browser close
Failed loginsLocked after 5 attempts for 15 min, tracked on address and identity together
Rate limitsLogin 5/min, password reset 3/hour, registration 10/hour
Analytics cache1 h, partitioned by organisation in the key, invalidated on progress events
RetentionSoft-deleted records purged at 90 days; audit trail kept 2 years

Scale

12Backend domains
45Database tables
96Migrations
117Learning outcomes
1019Automated tests
127React components

Approach

Structure before scale.

Map reality before proposing technology

The data model came out of how these organisations already work — roles, ranks, development levels, the real shape of a ranger career — not out of a framework's defaults. Where the existing process was sound the system encoded it. Where it was broken, that was named as a process problem rather than quietly automated.

Systems before features

Four capabilities that could each have been built as a separate product were built as views onto one competency framework. That is why the figures agree across screens, and why the fifth consumer will be cheap.

Build for the constrained case

The intended operators have limited technical capacity, distributed sites and real turnover. The system is containerised for predictable deployment, documented for people who did not build it, and structured so knowledge lives in the platform rather than in whoever currently holds it.

Say what is actually true

Coverage figures were corrected downward before they were improved. A gap of unknown size is reported as zero rather than complete. Two readiness scores are kept visibly distinct rather than averaged into one comfortable number. A system that flatters its operators is not a measurement system.

Outcome

A system that can be handed over.

The platform runs end to end: organisational structure, role requirements, the competency framework, progress tracking, gap analysis, career pathways, a shared training catalogue, a funding workflow, and the dashboards above them. Reporting, deployment and security hardening are complete; what remains is documentation and sign-off rather than development.

Scope grew materially over the engagement — a funding-application subsystem, a cross-organisational coordinating role, shared career ladders and a provider-facing dashboard all arrived after the original brief was met. The measure of the work is not that it launched. It is that it can now be operated, extended and understood by people who were not there when it was built.

Technology is never the objective. Better organisational capability is. Leadwood Systems