A GOV.UK-standard prototype for a regulatory notification service
When a fund's net asset value is calculated wrongly, someone has to decide fast whether the error is material. This prototype takes that judgement off the user.
Enter 0.85 at the error-percentage question for the material route, or 0.12 for the non-material route.
Important
This is a prototype, not a real service. It is not affiliated with the FCA or any regulator, and it does not submit data anywhere. Fund names, firm names and reference numbers are invented. Built to demonstrate service design and interaction patterns.
- The problem
- The service journey
- The materiality decision
- How it is built
- Screens
- Running it
- Deployment
- Patterns used, and why
- Accessibility
- Testing
- What I would test next
- Deliberately not built
- Repository structure
- Context
A UK authorised fund publishes a price. If that price was wrong — a stale asset valuation, a fee applied twice, a deal struck at the wrong valuation point — the manager has to work out how serious it is.
That single judgement changes everything downstream:
| If the error is material | If it is not |
|---|---|
| Notify the regulator within 5 business days | Record it in the breaches register |
| Pay redress to affected investors | Correct the affected valuation points |
| Evidence root cause and control changes | Tell the depositary |
In practice the decision often falls to whoever spotted the error first — frequently an operations analyst at a third-party administrator rather than a compliance specialist — working under time pressure, against guidance they may not have read that week.
It is a judgement with a defined numerical threshold. That makes it exactly the kind of judgement a service should make on the user's behalf.
Twelve pages. One question per page. The journey splits once, on a value the service calculates rather than a question it asks.
| # | Page | Purpose |
|---|---|---|
| 1 | / |
Start page — who can use it, what you need, the 5-day deadline |
| 2 | /reference-number |
Fund product reference number (PRN) |
| 3 | /fund-details |
Confirm pre-held register data — catches a wrong PRN early |
| 4 | /breach-type |
Pricing error, late trade, asset mispricing, fee error, box management |
| 5 | /breach-dates |
First and last affected valuation points, plus identification date |
| 6 | /breach-value |
Error as a percentage of NAV, and monetary value |
| — | branch | Materiality calculated in app/routes.js |
| 7a | /material-breach |
Obligations, task list, deadline explanation |
| 7b | /non-material-breach |
What still applies, prospectus warning |
| 8 | /investor-impact |
Investors affected; redress and root cause on the material route only |
| 9 | /check-answers |
Declaration before submission |
| 10 | /confirmation |
Reference number and route-specific next steps |
| 11 | /design-notes |
Design rationale, patterns, what to test next |
| 12 | /start-again |
Clears the session for repeat demos |
This is the point of the prototype.
The obvious question to ask is "Is this a material breach?" with a yes/no radio. That would be a mistake:
- it asks the user to interpret regulatory guidance under time pressure
- it produces inconsistent answers across firms, making the collected data useless for supervision
- it hides the reasoning, so nobody can audit how the conclusion was reached
Instead the service asks for the error as a percentage of net asset value, applies the 0.5% threshold itself, and then tells the user which route they are on and why — in one sentence, before they commit to a longer journey.
Two consequences worth noting:
- Every notification is assessed identically. The regulator gets comparable data rather than a field full of firm-specific interpretation.
- The non-material route still explains what to do. "Not material" is not "nothing to do" — the record, correction and depositary notification all still apply. A service that just said "you don't need to report this" would be actively misleading.
There is also a warning on the non-material route that a fund's prospectus can commit it to a stricter threshold than the guidance, which then overrides the default.
Implemented in app/routes.js as one commented function:
const MATERIALITY_THRESHOLD = 0.5
function parsePercent (raw) {
if (raw === undefined || raw === null) return null
const cleaned = String(raw).replace(/[%,\s]/g, '')
if (cleaned === '') return null
const value = Number(cleaned)
return Number.isFinite(value) ? value : null
}
router.post('/materiality', (req, res) => {
const percent = parsePercent(req.session.data.errorPercent)
if (percent === null) {
// No usable figure — send the user back rather than guess a route.
return res.redirect('/breach-value')
}
req.session.data.errorPercentParsed = percent
req.session.data.isMaterial = percent >= MATERIALITY_THRESHOLD ? 'yes' : 'no'
return res.redirect(
req.session.data.isMaterial === 'yes' ? '/material-breach' : '/non-material-breach'
)
})Three deliberate details:
- Forgiving parsing. These figures get pasted out of spreadsheets, so
"0.75 %","1,200"and stray whitespace all parse. Rejecting a valid number because of a trailing percent sign would be a self-inflicted wound. - Decided once, stored once. The outcome is written to the session so later pages and check-answers cannot disagree with each other.
- No guessing. Unusable input returns the user to the question rather than defaulting to a route. A live service would render an error summary here.
| Layer | What it does |
|---|---|
| Views | Nunjucks templates, one per page, extending a shared GOV.UK-branded layout |
| Routing | A single POST /materiality handler holding the branch logic |
| Session | Prototype Kit session data — prn, breachType, errorPercent, isMaterial |
| Framework | GOV.UK Prototype Kit 13.20 on Express, GOV.UK Frontend 6.4 components |
Components used: input · radios · checkboxes · date-input · summary-list · task-list · notification-banner · warning-text · panel · details · character-count · table · tag · inset-text · back-link · button
Start page
|
Question page
|
The value question — where the branch is decided
|
Material route — 0.85% entered
|
Non-material route — 0.12% entered
|
Check your answers
|
Design notes
|
Mobile
|
The quickest option is the live deployment — no setup needed.
To run locally, Node.js 22 or later:
git clone https://github.com/davendra/fundadmin-gov-prototype.git
cd fundadmin-gov-prototype
npm install
npm run devAt the error-percentage question:
| Enter | Route | What you should see |
|---|---|---|
0.85 |
Material | Important banner, task list, 5-day deadline, redress and root-cause questions later |
0.12 |
Non-material | Below-threshold explanation, prospectus warning, no redress questions |
0.5 |
Material | Boundary case — the threshold is inclusive |
0.75 % |
Material | Forgiving parsing: the stray percent sign and space are stripped |
| (blank) | — | Returns you to the question rather than guessing |
/start-again clears the session between demos.
Live at fund-admin.gov.davendra.com on Vercel. The Prototype Kit assumes a writable filesystem and a long-lived process, and neither holds on serverless, so three things are adapted:
Sessions move to a signed cookie. The Kit stores session data as JSON files under .tmp/sessions. A serverless function has no shared filesystem between invocations, so file-backed sessions silently lose data — which would break the materiality branch, since it depends on the stored decision surviving a redirect. cookie-session is registered ahead of the Kit's own middleware, so the whole session travels in a signed cookie and any invocation can read it.
Sass is compiled at build time. The Kit compiles stylesheets at boot, which fails on a read-only filesystem. scripts/vercel-build.js calls the Kit's own generateAssetsSync during the build instead, then the function serves the output as a static file. The build fails loudly if the stylesheet is missing or implausibly small, rather than deploying an unstyled prototype.
Runtime files are bundled explicitly. Vercel's static tracer cannot see that the Kit loads views and plugin manifests dynamically, so the first deploy returned 500s with an empty function bundle. includeFiles in vercel.json ships app/, .tmp/ and the Kit's packages, and KIT_PROJECT_DIR pins the project root because cwd is /var/task.
USE_AUTH=false disables the Kit's production password gate. That gate is correct for real government prototypes — it stops unfinished work being published — but this is a public portfolio piece, so the pages carry their own prototype notices and the deployment sends X-Robots-Tag: noindex, nofollow.
| Pattern | Where | Reason |
|---|---|---|
| One thing per page | Throughout | Each question can be answered from a different source document |
| Check answers before you submit | Before sending | This is a regulatory declaration — the user attests to accuracy |
| Confirm pre-held data | Fund details | Reference-number lookups fail silently; showing the fund name catches a wrong PRN |
| Branch on a calculated value | After the value question | Removes a regulatory judgement from the user |
| Task list | Material route only | That journey is long enough that users will leave and return |
| Character count | Root cause | Free text has a downstream length limit; showing it prevents lost work |
| Warning text | Non-material route | A prospectus can commit a fund to a stricter threshold |
| Details | Value question | Calculation help, available without cluttering the question |
Built to WCAG 2.2 AA intent, following GOV.UK Design System guidance:
- Question as
h1on every question page, so screen-reader users hear the question first rather than the service name - Every input has a visible label, programmatically associated — no placeholder-as-label
- Fieldset and legend around every radio and checkbox group
- Visually hidden text on change links in check-answers, naming what each link changes
- Prefix and suffix elements for
%and£rather than placeholder hints inputmodeset on numeric fields for correct mobile keyboards, without blocking paste- Materiality conveyed in text as well as tag colour — never colour alone
- Skip link, landmarks and heading hierarchy inherited correctly from the GOV.UK layout
Verified across question pages: h1 count 1, zero placeholder misuse, fieldsets present on all grouped inputs, 7 visually hidden change links.
The branch logic and both journeys were verified end to end:
| Test | Input | Expected | Result |
|---|---|---|---|
| Below threshold | 0.35 |
→ non-material | ✅ |
| At threshold | 0.5 |
→ material (inclusive) | ✅ |
| Above threshold | 1.2 |
→ material | ✅ |
| Messy input | "0.75 %" |
parsed → material | ✅ |
| Empty input | "" |
→ back to question | ✅ |
| Non-numeric | "abc" |
→ back to question | ✅ |
| Session persistence | PRN 445591 |
shown on check-answers | ✅ |
| Conditional fields | non-material route | redress + root cause absent | ✅ |
| Dynamic back link | non-material route | points to /non-material-breach |
✅ |
| Route-specific outcome | both routes | different confirmation next-steps | ✅ |
All 12 pages return HTTP 200 with no template rendering errors.
With real users, in this order — and I would expect to be wrong about at least one of these:
- Does the percentage question work at all? The assumption is that users have this figure to hand. If they arrive with a monetary error and no percentage, the calculation belongs in the service and the question order is wrong.
- Is one error percentage enough? Multi-class funds may have a different error per share class. If users cannot answer without splitting it, this becomes a repeating pattern — a change that would ripple through the whole journey.
- Is the calculated result trusted? If users override the service's answer or ring the regulator anyway, the explanation is not doing its job.
- Does the deadline land? The 5 business day clock starts at identification, not submission. Whether the current wording conveys that is testable, and I would expect to get it wrong first time.
Being explicit about scope, because a prototype that pretends to be production-ready is worse than one that admits its edges:
- Server-side validation with error summaries and inline messages
- A real Financial Services Register lookup — the fund details are stubbed
- Save and return, and authentication
- The repeating pattern for multi-share-class funds
- Any actual submission, storage or notification
app/
├── config.json Service name and plugin config
├── routes.js The materiality branch — the interesting file
├── filters.js Nunjucks filters (unused)
├── data/
│ └── session-data-defaults.js
└── views/
├── layouts/main.html Extends govuk-branded
├── index.html Start page
├── reference-number.html PRN
├── fund-details.html Confirm register data
├── breach-type.html Radios with hints and divider
├── breach-dates.html Three date inputs
├── breach-value.html Percentage and monetary value
├── material-breach.html Material route
├── non-material-breach.html Non-material route
├── investor-impact.html Conditional questions by route
├── check-answers.html Summary lists with change links
├── confirmation.html Panel and route-specific next steps
└── design-notes.html Rationale and testing plan
docs/images/ Diagrams and screenshots
Built as a working demonstration of GOV.UK service design applied to a regulated financial-services problem.
The domain is deliberate. I spent five and a half years leading AI and automation at a global fund administrator, so the fund valuation, NAV, depositary and materiality concepts here are drawn from that work rather than researched for the exercise. Wiring up Design System components is straightforward; knowing which question not to ask is the part that comes from the domain.
The diagrams were generated with Go Bananas, an AI image platform I built and run.
Davendra Patel
davendra.com · github.com/davendra · linkedin.com/in/davendra-patel










