Case study · Supply chain risk management

Can a nightly feed and a live decision share one register?

A global automotive supplier was buying supplier risk intelligence and getting alerts it could not close. The data landed in a warehouse, the decisions happened in meetings, and nothing connected the two. We built the layer in between.

94 risk types routed automatically
4 deadline tiers, system-owned
21 audited action types
Zero decisions overwritable by the sync
StatusDelivered
0
risk types routed to 4 responsible functions
0
deadline tiers applied automatically
0
audited action types, ownership checked per write
0
specialist decisions the nightly sync can overwrite
Two views in, one register

Two warehouse views ingested nightly, de-duplicated and normalised into one working register.

Nothing to remember

The five-business-day screening deadline on a Critical alert is enforced by the system, not by a reminder.

A boundary, not a truce

Machine-owned and human-owned columns separated at schema level, in both directions.

Encoded, not documented

Seven screening outcomes, five case statuses, three governance routes and six governance outcomes, enforced rather than filed.

The client

A programme with a method, and nowhere to keep it.

A global automotive supplier operating a supply chain risk management programme across four operating divisions.

Upstream

Where the alerts come from

Commercial risk intelligence services, and the client's own enterprise data warehouse. Two curated views, refreshed on a daily cycle.

The programme

Who works in it

Risk specialists, risk owners, segment managers and governance councils, across four operating divisions.

Downstream

Where the decisions go

The enterprise reporting estate, which reads the curated register back out of the warehouse.

Where this starts

The programme was already producing judgements. What was missing was anywhere to put them that the next refresh would not destroy.

The challenge

Volume looks like health.

Commercial risk intelligence is excellent at detection and silent on resolution. Alerts arrived continuously, a factory fire, a credit downgrade, a sanctions listing, a labour dispute, and landed as warehouse rows nobody owned. An alert nobody had looked at was indistinguishable from one that had been cleared.

Could not show 01

An unworked alert was invisible

Nothing distinguished an alert nobody had opened from one reviewed and dismissed. The oldest items were the least likely to be noticed, because the list was sorted by arrival and read from the top. Ageing was a property of the data that nothing in the process measured.

Could not show 02

A refresh and an edit could not share a row

The feed refreshed supplier and event attributes continuously. The team wrote decisions onto the same records. With no ownership boundary the two were in direct competition: either the refresh overwrote judgement, or judgement was kept somewhere the refresh could not reach, which meant somewhere the reporting could not reach either.

Could not show 03

A decision without its reasoning could not be defended

Supply chain due diligence is a regulatory obligation in several of the client's markets. "The council approved the mitigation" was not an answer. Which council, on what severity, derived from which impact and likelihood ratings, against which of six defined outcomes, recorded by whom: that is an answer, and none of it was structured data.

Before the build
2
sources of supplier risk intelligence fed one warehouse, and stopped there.
1
spreadsheet stood between the feed and the governance council.
0
enforced deadlines before the build. Screening happened when someone got to it.
0
structured records of a governance decision existed.
The objection this answers

The feed is working, the alerts are arriving, the team is busy. But a register where every row is present is not the same as a register where every row has been decided. Ten thousand alerts can still represent one unanswered question.

The methodology

A methodology that already existed, on paper.

The client was not starting from nothing. There was a risk management policy, with impact and likelihood criteria in an appendix, a severity matrix in another, a Risk Council with a defined remit, and specialists who knew the work.

What there was

A documented policy, rating criteria, a severity matrix, a governance council with a remit, and a live risk feed against supplier master data.

What there was not

A system that held any of it. The criteria lived in a document. The matrix was applied by hand. The council's decisions were minuted and then effectively unsearchable. The register itself was a spreadsheet that reflected whatever its last editor believed.

What was already in place
Live risk feed
Supplier master data
A documented methodology
A governance council
No system of record
The mandate

Four questions, and the fourth decided the design.

Ingest
How does a warehouse feed become a list a specialist can actually work?
Own
Which columns belong to the machine, and which belong to the person?
Enforce
What should the system decide, and what must stay a human judgement?
Prove
Can any decision be reconstructed, in full, months later?The fourth is the one that shaped everything above it.
The schema

A schema that separates what the machine owns from what the person owns.

Instead of asking a nightly job and a risk specialist to take turns. Every alert row carries two kinds of content, and the boundary between them is enforced from both directions.

The machine owns

Refreshed from the warehouse on every run

The event identifier, title, date identified, source system, priority, affected suppliers and risk type. The nightly sync updates only these columns, and only on machine-created rows.

The person owns

Untouchable by any refresh

The screening decision, assigned specialist, case number, risk owner, severity, governance outcome and notes. The API refuses user edits to machine-owned columns, and the sync cannot reach a single one of these.

The exception, and why it is safe

An alert created by hand inside the application is flagged as such and is fully editable, because no upstream system owns it. Nothing about the feed changes. The past data stays valid. Only the register becomes safe to work in.

What the register answers
Has this alert been screened?
A screening decision that is never blank, defaulted at both application and database level.
Is anyone accountable for it?
An assigned specialist, checked per record on every write.
Is it late?
Screening Overdue, computed nightly against a business-day allowance.
How severe is it?
The output of a four by four impact and likelihood matrix, not a typed-in value.
Who decides it?
A governance route derived from that severity, so assessment and escalation can never disagree.
What was decided?
One of six defined governance outcomes, recorded against the case.
Who changed it, and when?
21 audited action types with the acting user's identity.
Can the sync run safely tonight?
Yes. It cannot reach a single human-owned column.
Architecture

Two warehouse views, one register, four layers.

Snowflake stays the warehouse. Entra ID stays the identity provider. The client's existing document estate stays the document estate. One PostgreSQL database is the only new store introduced, and the ingestion job ships inside the application's own container image, so the two can never drift apart.

Warehouse source to case file
Source
Two Snowflake viewsSupplier master data and the supplier risk event feed, curated upstream, read over a key-pair authenticated connection with explicitly named columns
Landing
A one-to-one mirrorEach view mirrored in PostgreSQL, owned entirely by the sync job, upserted on natural keys so a re-run is always safe
Register
One row per risk eventFanned in from the feed's per-supplier rows, with the machine-owned and human-owned split applied. This is what users work in
Case file
The mitigation recordIts assessment, governance decision, action plan and supplier drilldown, linked to one or many alerts

The curated register is then read back into Snowflake by the client's data platform for enterprise reporting, the loop that turns what happened into what was decided about it.

Why once a night, and not more often

The sync runs once a day because the source does. Snowflake refreshes these views on a daily cycle, so a more frequent poll would re-read identical rows and buy nothing.

The cadence is inherited from the warehouse rather than chosen, and it is what makes a single overnight pass the right shape: by the time the working day starts, the register already holds everything the warehouse knows.

Four workstreams, one application
Ingestion and normalisation
The working register
The mitigation case file
Governance and audit

Delivered as one application with a scheduled job, not as a platform.

The register

Delivered surface, module by module.

The register
Twenty four configurable columnsPer-user layout persisted server-side, continuous cursor-based loading with row virtualisation, grouping on any major column, compound filters shown as removable chips, and a live summary strip computed across the whole dataset rather than the filtered view.
Screening
Seven outcomes, one system-owned statePer-priority business-day allowances applied on every nightly run, with an overdue state no user can set or clear.
The case file
Six sections mirroring the client's methodologyRisk overview, severity assessment, governance decision, operational snapshot, mitigation strategy and key updates, with a tracked action list.
Supplier context
A searchable hierarchyGroup trees, live master-data lookup on case supplier selection, and a denormalised per-case drilldown.
Governance and audit
Three roles, per-record ownership checksAn administrator-only audit view with compound filtering, and a maintained specialist roster.
Built, and switched off

Each case records its externally hosted document folder, mirrored onto every linked alert. A complete cloud object storage implementation with folder trees, upload, move, rename and replace is built and currently switched off, in favour of the client's existing estate.

One alert

The life of one alert, end to end.

01
A risk intelligence service detects an event at a supplier and it lands in the warehouse.
02
At 06:00 UTC the sync reads it, folds forty per-supplier rows into one event with a supplier list, normalises the priority label, and derives the responsible function from the risk type.
03
It appears in the register as Screening Required, unassigned, with a deadline already running.
04
A specialist claims it, reviews it, and records exposure identified under one of four routes.
05
They open a mitigation case. The application issues the number under a database lock; two specialists working at the same second cannot collide.
06
They rate impact across six categories and likelihood against on-screen criteria. The matrix computes severity. The severity determines the escalation route. Neither is typed in.
07
The Risk Council reviews and the outcome is recorded against the case. Mitigation actions are tracked with owners and target dates.
08
Residual severity is recorded post-mitigation and mirrored back onto every linked alert. The case closes, and the whole sequence is reconstructible from the audit trail.
The trust layer

What the interface shows has to match what the system will allow.

Permissions

Ownership, not seniority

Three roles. Viewers read everything. Specialists claim any alert and edit the ones assigned to them. Administrators manage the roster and read the audit log, and hold no extra power over cases whatsoever. An administrator who wants to edit an alert must assign it to themselves first, which leaves a trace. Ownership is evaluated per record on every write, never once at login.

Identity

It survives deletion

A specialist's display name is captured onto the alert at the moment of assignment, alongside their address. When they leave and are removed from the roster, historical records still read correctly. The audit trail does not develop gaps because somebody changed jobs.

The clock

The overdue state belongs to nobody

No user can mark an alert overdue, and no user can move one back out of it. The allowance has either elapsed or it has not. Removing the human hand from that one field is what makes the overdue count on the summary strip worth reading.

The interface

It reports capability

Read-only cases open read-only, rather than letting a specialist fill in a full assessment and discover the refusal on save. What the interface shows has to match what the system will allow.

Both sides of the trade are priced
The system owns

The clock and the arithmetic. Deadlines, derived owners, computed severity and escalation routing are automatic.

The specialist owns

The judgement. Whether exposure exists, how severe the impact is and what the mitigation should be are never automated, and the audit trail records a person's name against each one.

Every value is the client's own

The impact categories, rating criteria, severity matrix and escalation routes come from the client's existing risk policy appendices. Nothing was invented. The rating criteria are available on screen at the moment of rating, so the policy is applied where the decision is made rather than filed where it can be ignored.

Before cutover

A register that governs a regulated process does not go live because it demos well.

A governance system that needs its own governance is a second problem. This one runs on controls the client already operates.

The service target
The nightly sync completes before the working day starts
No specialist decision is ever lost to a refresh
A schema fault fails the deployment, not the application
A database blip never restarts a healthy service
Design intent
A nightly refresh can run against live case work without destroying it.
What was built
Column ownership enforced in the sync's update clause and in the API's edit validation, in both directions.
Deployment model
Images tagged by build identifier, pulled by exact tag, never a floating latest.
Blast radius
Migration faults fail the deploy; database faults do not restart healthy containers.
Outcomes

What the build actually produced.

The core result
Eleven changes of state
System of record
A spreadsheet
PostgreSQL registerten tables, twenty five indexes
Screening deadlines
None
Four tiersapplied nightly, system-owned
Ageing visibility
Sort by date and hope
Overdue counton the summary strip, computed globally
Severity determination
Typed in
Computedfrom a four by four impact and likelihood matrix
Escalation route
Chosen
Derived from severityand so it cannot disagree with it
Risk type routing
Manual
94 typesmapped to 4 responsible functions automatically
Supplier context
Name lookup elsewhere
Full hierarchygroup trees, live master data
Governance record
Meeting minutes
Six outcomesstructured, recorded against the case
Audit trail
None
21 action typeswith per-record identity
Data refresh safety
Overwrite or re-key
Ownership boundaryneither side can damage the other
Reporting input
Raw alert feed only
Curated decision recordreturned to the warehouse
The reusable pattern

Any regulated workflow where an automated feed meets expert human judgement.

Separate machine-owned from human-owned data at schema level, enforce the boundary from both directions, compute everything the governing methodology already determines, put a clock on the human step and take that clock out of human hands, and record every mutation against an identity that survives the person leaving.

Where else this shape fits
Supplier compliance and due diligence
Financial crime alert triage
Pharmacovigilance case management
Quality and safety incident handling
ESG and human rights screening
Technology stack

Nine layers, one application.

Front end

RE
React 18
TY
TypeScript
VI
Vite
TV
TanStack Virtual
MB
MSAL Browser

Back end

PY
Python
FA
FastAPI
UV
Uvicorn
SQ
SQLAlchemy 2.0
PY
Pydantic

Database

PO
PostgreSQL
NA
Native array columns
CI
Composite indexes
CP
Cursor pagination

Ingestion

SC
Snowflake Connector for Python
KP
Key-pair JWT auth
IU
Idempotent upserts

Warehouse

SN
Snowflake

Authentication

ME
Microsoft Entra ID
PR
PyJWT RS256 validation
PR
Per-record authorisation

Storage

AB
Azure Blob Storage
AI
Azure Identity
UD
User-delegation SAS

Infrastructure

DO
Docker
DC
Docker Compose
NG
Nginx
AC
Azure Container Registry

CI/CD

AD
Azure DevOps Pipelines
SH
Self-hosted agents
MP
Migration pre-flight
SR
Scripted rollback
Built, currently disabled

The Azure Blob Storage layer, with Azure Identity and user-delegation SAS, is complete and switched off in favour of the client's existing document estate.

Let's build

Is your risk feed producing alerts nobody can close?

Detection is the easy half. If the decisions live in meetings and the data lives in a warehouse, the layer in between is the one worth building.