Engineering, security and assurance

How WealthHorizon is built

A firm putting client records into a system is entitled to know how that system is made, what it is tested against, and what it does not claim. This page states the standards the build is held to, the controls that are implemented, and — at the end — the limitations that are known and deliberate.

121ordered, idempotent database migrations, applied automatically on start
167backend domain services, one per bounded context, holding all the SQL
69feature pages in the application, code-split by route
5,883automated tests across the two suites, all required to pass to merge

1. The stack

Current, supported versions — and a stated reason for anything held back

Both halves of the application are written in TypeScript under strict, and compile to ES2022. Dependency updates are taken in verified batches rather than one at a time, because a lockfile that has never been resolved against its siblings has not been tested.

Languages, runtimes and libraries.
LayerTechnologyVersion
Front-end frameworkReact18.3
Build toolVite6
LanguageTypeScript, ESM, strict: true5.7
Component libraryFluent UI v9 (Fluent 2), Griffel CSS-in-JS9.x
Server runtimeNode.js (LTS)22
Web frameworkExpress4.21
Request validationzod, at every route boundary3.24
Password hashingargon2 (argon2id)0.41
DatabasePostgreSQL16
Database drivernode-postgres8.13
Unit and integration testsVitest (both packages)4 / 2
Component testsjsdom + React Testing Library
End-to-end testsPlaywright, with phone and tablet device profiles
DeploymentDocker Compose — database, API, web

Held back deliberately. Three major upgrades are outstanding, each for a stated engineering reason rather than caution: Express 5 changes the type of route parameters and the routing engine, and needs its own change route by route; Vite 8 swaps the bundler, which changes what is emitted and needs a build step added to continuous integration first; and the React plugin upgrade is paired with it. Recording why a version is not taken is part of the standard.

2. How a change is made

Specification first, then a failing test, then the smallest change that passes it

The development standard for this codebase is written down and applies to every non-trivial change. It optimises for verified behaviour rather than apparent progress.

1 · Understand and specify

The goal is restated, the expected behaviour identified, and the edge cases, failure modes and backwards-compatibility risks named — against the existing code and tests, not against an assumed architecture.

2 · Test plan, then failing tests

A plan covering the happy path, boundaries, error cases, regression risks and any security or data-integrity behaviour. The tests are written and run before the production change, and must fail for the expected reason.

3 · Minimal implementation

The smallest production change that passes the tests, following existing conventions. Broad opportunistic refactoring during a feature or a fix is not permitted.

4 · Refactor under test

Naming, structure and duplication are improved once the tests pass, with behaviour unchanged and the tests re-run afterwards.

5 · Quality gate

Unit tests, affected integration tests, type checks, lint and formatting, and the build. Anything that cannot be run is stated explicitly, along with what should be run by hand.

6 · Documentation in the same change

Documentation is a deliverable, not a follow-up. A change to a page, route, role or feature updates the in-application help, the relevant user guide and the architecture documents in the same change.

Four rules are absolute: a test is never weakened, skipped or deleted to make a suite pass; coverage is never removed without a stated reason; public behaviour never changes without a test; and no claim is made that tests pass unless they were run.

3. Testing and assurance

Five gates, every one of which blocks a merge

Continuous integration runs on every pull request. A change that fails any of the following cannot be merged.

  • Type check — front end The whole SPA must compile clean under strict with no emit. Type errors are a build failure, not a warning.
  • Type check — back end The same, independently, for the API and its services.
  • SQL against the real schema Every migration is applied to a live PostgreSQL 16 instance to build the actual schema, and then every static query in the backend is PREPAREd against it. Preparing validates every table and column reference without executing anything — so a query referring to a column that does not exist fails the build. This is the class of defect a type checker cannot see, and it is caught before merge rather than in production.
  • Front-end test suite 2,693 tests across 224 files — pure calculation helpers, formatting and responsive logic, and component behaviour under jsdom.
  • Back-end test suite 3,190 tests across 246 files, run against a migrated and seeded PostgreSQL instance so that the database-backed tests exercise real data rather than a mock.

What the tests are pointed at

Testing favours pure functions with no database dependency, because that is where the arithmetic lives and where a defect is silent. Financial software fails quietly: a wrong answer is still a number. The suites therefore concentrate on the calculations that produce a figure a person will act on —

  • Foreign exchange conversion and the historic rate held per transaction.
  • The tax engine, allowances and the capital gains matching rules.
  • Long-term projection, drawdown ordering and the Monte Carlo simulation.
  • Lot tracking, corporate actions and the account valuation rule.
  • Compound interest in the savings estimator — where taking a twelfth of a rate rather than its twelfth root pays 12.68% a year on a screen that says 12%.
  • Import parsers, balance reconciliation and duplicate detection across every supported file format.
  • Backup encryption, round-tripped and verified including the authentication tag.
  • Role visibility — that an adviser cannot see a client they are not engaged with, and that the audit log refuses every caller but compliance.

Import readers are additionally verified against real broker and bank files rather than synthetic ones: complete files from Trading 212, Saxo, Barclays and QIF sources are imported and reconciled row for row, and a row that cannot be read is reported rather than dropped.

4. Security architecture

The controls, and where each is enforced

Authorisation is enforced in two layers: in the application services, where a refusal can be explained, and in the database as constraints and triggers, so that a defect or a direct connection cannot bypass it.

Implemented controls.
ControlImplementation
Identity and session
Password storageargon2id hashing. Plaintext passwords are never stored or written to a log.
Password policyAt least ten characters with upper case, lower case, a digit and a symbol, and a common-password list rejected.
Session tokensShort-lived signed access tokens, default fifteen minutes, refreshed transparently. Refresh tokens are 32 random bytes; only a SHA-256 hash of one is ever stored.
RevocationSigning out revokes the presented refresh token. A password reset revokes every refresh token for that user, on every device.
Two-factor authenticationOptional per user, time-based one-time password, enforced at sign-in once verified.
Address verificationSingle-use, time-limited e-mail verification where outbound mail is configured; auto-verified where it is not, so an offline installation cannot lock out its own operator.
Account enumerationPassword reset and verification resend always answer success, whether or not the address is registered.
Reset token handling32 random bytes, stored only as a SHA-256 hash, valid for one hour, single use. A weak replacement password is refused before the token is consumed.
Abuse limitingPassword-reset requests are limited per address, and the address is hashed in the counter so the list of people who forgot a password is not held in the clear in order to count to five.
Authorisation and isolation
Tenant isolationEvery record carries its tenant; every query is scoped to it. Staff acting on a client supply an explicit client context, which is checked against an active relationship before any data is returned.
Role-based accessRoles and permissions are data, not code branches: standard user, adviser, compliance manager, hierarchy manager, system administrator. Navigation and routes are both filtered, and the API refuses independently of the interface.
Administrative invariantsThe system always retains at least one active administrator; the first administrator cannot be deleted, demoted or deactivated. Enforced in the service layer and again by database triggers, each with its own error code.
Deletion safetyDeleting users is planned across the whole selection rather than row by row, so two administrators cannot each pass a “not the last one” check and together leave none. Every reference to a user is classified from the live database catalogue, so a table added later cannot reintroduce the defect.
Data and integrity
Input validationzod schemas at the route boundary, with database constraints and enumerated types as the backstop. Validation failures return a structured error, never a stack trace.
Backup encryptionAES-256-GCM with a random per-bundle salt and initialisation vector, and a key derived with PBKDF2-SHA256 over 100,000 iterations. Authenticated encryption means a wrong passphrase fails the tag check rather than producing plausible rubbish.
Backup lifecycleScheduled daily, weekly or monthly, with a retention count that prunes older snapshots and a log of every run.
Audit trailAuthentication, role grants, client record access, planning access, suitability generation and backups are recorded with actor, action, resource, time and address. Writing an audit entry can never fail a request.
Outbound and content
Server-side request forgeryThe only outbound fetch of a user-supplied address — an institution’s logo — passes an allowlist of shapes rather than a blocklist of dangers: HTTP and HTTPS only, ports 80 and 443 only, no embedded credentials, and a public host. Addresses are judged numerically, so obfuscated forms of loopback and private ranges are refused, as are link-local and cloud-metadata addresses in every IPv4-mapped form. A bare name with no dot is refused because that is another container. Every redirect hop is re-checked.
Content typeThe bytes decide the type, never the upstream header. Only known image signatures are stored; SVG is refused because it is a scripting-capable document and an active document served from our own origin is cross-site scripting. Stored images are served with no-sniff, a restrictive content-security policy and a same-site resource policy.
SecretsSupplied as environment variables and required at start, so a deployment missing one fails immediately rather than running insecurely. Configuration files are excluded from version control and from the release package; deployment scripts generate strong random values where none is supplied.
Demonstration dataEvery demonstration account and fixture is behind an explicit flag that defaults to off. A production installation ships with no login accounts at all; the first person to register becomes the first administrator.

5. Data protection

Designed for a UK GDPR and Data Protection Act posture

Data minimisation and purpose limitation

The client-facing access record returns deliberately limited fields — who, what, when — and never firm-wide detail. The firm-wide audit trail is a separate, separately restricted view.

Integrity and confidentiality

argon2id password hashing, hashed session tokens, authenticated backup encryption, tenant isolation, and access invariants enforced at the database as well as in the application.

Accountability

An activity trail covering access to and change of client records, including access by staff, available to the compliance function and, in its client-scoped form, to the client themselves.

Residency and sovereignty

The platform runs self-contained on infrastructure of the firm’s choosing. Backups remain inside the database volume. There is no requirement for data to leave the jurisdiction, or the building.

These are platform capabilities that support compliance. The operating firm remains the data controller and is responsible for its own data protection impact assessment, lawful basis, retention policy and subject access request handling.

6. Accessibility and devices

Requirements enforced globally, not decided per screen

  • Target sizeInteractive elements meet a 44×44 pixel minimum on touch devices, applied by a global rule rather than left to each component. A form control counts as one target, not several.
  • Visible focusEvery custom interactive element shows a focus ring on keyboard focus. Removing an outline without providing an equivalent is not permitted.
  • Keyboard operabilityAnything that behaves as a button is operable by keyboard: a tab stop, and Enter and Space firing the same handler. This covers clickable table rows, cards, collapsible headers and metric tiles.
  • SemanticsNavigation marks the current page, collapsible groups announce their state, and icon-only controls carry an accessible label.
  • Reading and zoomInputs use a 16 pixel minimum font size on mobile so that focusing a field does not zoom the page.
  • Physical screenSafe-area insets keep content clear of a notch, a dynamic island and a home indicator. Full-height layouts use dynamic viewport units so content is not clipped behind a browser toolbar that moves.
  • Device envelopeiPhone 13 and newer and equivalent Android phones; iPad Air 2020 and newer in both orientations; desktop and large desktop. Below tablet width the navigation rail becomes a drawer and wide tables scroll within themselves rather than overflowing the page.
  • VerificationEnd-to-end tests run against phone and tablet device profiles as well as desktop.

These implement the WCAG 2.2 success criteria for keyboard operability, visible focus and target size. No third-party conformance audit has been commissioned, and no formal conformance level is claimed.

7. Deployment and ownership

Three containers, no vendor lock-in

The whole system is a PostgreSQL database, a Node API and an nginx-served single-page application. Because that is all it is, the same artefact runs unchanged on a self-hosted machine, a network-attached appliance, any public cloud, or a managed container platform.

The database schema is applied by the API on start-up. The runner is idempotent — applied files are recorded and skipped, and a file that errored is re-attempted next time — and understands complex SQL, treating a semicolon as a terminator only at the top level so that function bodies and string literals survive intact.

Every build carries a version stamp committed with the source rather than derived at deploy time, so the number on the screen identifies the exact build running.

Licensing and provenance

WealthHorizon is proprietary software owned by Muneris Management Ltd. All rights reserved. It is not open source, and no licence is granted by access to the source; use is governed by a separate written agreement.

Ownership is maintained through three mechanisms:

  • A copyright and proprietary notice in every source file, applied and maintained by tooling.
  • An origin sign-off on every commit, certifying provenance and the right to submit under the stated terms.
  • Intellectual property assignment terms binding every authorised contributor, so that ownership does not fragment.

8. Known limitations

What is not claimed

A security position that lists only its strengths is not a security position. The following are known, documented, and stated here because a firm assessing the platform should assess it accurately.

  • Sign-in rate limiting Password reset is rate limited; sign-in is not. Behind a reverse proxy the only address the server can see is the proxy’s, so a per-address limit collapses to a single shared bucket that would lock out a whole household, and a per-account limit would hand anybody a way to lock a named person out of their own money. This requires a deployment-level decision about trusting a forwarded address, and it is treated as one rather than papered over.
  • Account identifiers A sort code or account number entered by a user is stored as entered. Nothing masks or encrypts that field, and it is therefore present in backups, which are themselves encrypted at rest where a passphrase is set. Both the in-application help and the user guide say so and recommend recording only the last few digits. A firm requiring a stronger control here should mask on write or hold the field encrypted; that is a change, not current behaviour.
  • Row-level security Tenant isolation is enforced in the service layer, where every query is scoped by tenant. The schema also ships illustrative PostgreSQL row-level-security policies, commented out, as a documented hardening path. They are not enabled today.
  • Open Banking The account-aggregation integration works against a provider’s sandbox and interface. Production use requires the regulatory permissions and provider onboarding that any account information service requires, and that is a firm-level undertaking rather than a configuration setting.
  • Broker connections The Trading 212 engine is built and the boundary is read-only, enforced by an allowlist from which the order endpoint is deliberately absent. Saxo remains at design stage. Reconciliation reports differences; it never applies them.
  • Vulnerability scoring The score is a weighted, explainable calculation over recorded signals. It is decision support for prioritisation. It is not a determination that a client is vulnerable, and it does not replace the judgement, the conversation or the record of the response.
  • Planning outputs Projections, forecasts and estate calculations are indicative and depend entirely on assumptions the user enters. They are not financial advice, a personal recommendation or a guarantee, and they are labelled as such in the product as well as here.